diff options
| author | Pliable Pixels <pliablepixels@gmail.com> | 2017-09-21 12:49:18 -0400 |
|---|---|---|
| committer | Pliable Pixels <pliablepixels@gmail.com> | 2017-09-21 12:49:18 -0400 |
| commit | b28028ac4082842143b0f528d6bc539da6ccb419 (patch) | |
| tree | 1e26ea969a781ed8e323fca4e3c76345113fc694 /www/lib/ionic/js/ionic.bundle.js | |
| parent | 676270d21beed31d767a06c89522198c77d5d865 (diff) | |
mega changes, including updates and X
Diffstat (limited to 'www/lib/ionic/js/ionic.bundle.js')
| -rw-r--r-- | www/lib/ionic/js/ionic.bundle.js | 8890 |
1 files changed, 2847 insertions, 6043 deletions
diff --git a/www/lib/ionic/js/ionic.bundle.js b/www/lib/ionic/js/ionic.bundle.js index 091bb875..75fbaecf 100644 --- a/www/lib/ionic/js/ionic.bundle.js +++ b/www/lib/ionic/js/ionic.bundle.js @@ -9,7 +9,7 @@ * Copyright 2015 Drifty Co. * http://drifty.com/ * - * Ionic, v1.3.3 + * Ionic, v1.2.4-nightly-1917 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * @@ -25,7 +25,7 @@ // build processes may have already created an ionic obj window.ionic = window.ionic || {}; window.ionic.views = {}; -window.ionic.version = '1.3.3'; +window.ionic.version = '1.2.4-nightly-1917'; (function (ionic) { @@ -580,7 +580,7 @@ window.ionic.version = '1.3.3'; * * `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/> * `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/> - * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, <br/> + * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, </br> * `touch`, `release` * * @param {string} eventType The gesture event to listen for. @@ -2113,11 +2113,6 @@ window.ionic.version = '1.3.3'; * @returns {string} What grade the current platform is. */ grade: null, - /** - * @ngdoc property - * @name ionic.Platform#ua - * @returns {string} What User Agent is. - */ ua: navigator.userAgent, /** @@ -2196,7 +2191,7 @@ window.ionic.version = '1.3.3'; self.platforms.push('webview'); if (!(!window.cordova && !window.PhoneGap && !window.phonegap)) { self.platforms.push('cordova'); - } else if (typeof window.forge === 'object') { + } else if (window.forge) { self.platforms.push('trigger'); } } else { @@ -2236,7 +2231,7 @@ window.ionic.version = '1.3.3'; * @returns {boolean} Check if we are running within a WebView (such as Cordova). */ isWebView: function() { - return !(!window.cordova && !window.PhoneGap && !window.phonegap && window.forge !== 'object'); + return !(!window.cordova && !window.PhoneGap && !window.phonegap && !window.forge); }, /** * @ngdoc method @@ -2397,7 +2392,7 @@ window.ionic.version = '1.3.3'; /** * @ngdoc method * @name ionic.Platform#showStatusBar - * @description Shows or hides the device status bar (in Cordova). Requires `ionic plugin add cordova-plugin-statusbar` + * @description Shows or hides the device status bar (in Cordova). Requires `cordova plugin add org.apache.cordova.statusbar` * @param {boolean} shouldShow Whether or not to show the status bar. */ showStatusBar: function(val) { @@ -2424,7 +2419,7 @@ window.ionic.version = '1.3.3'; * @name ionic.Platform#fullScreen * @description * Sets whether the app is fullscreen or not (in Cordova). - * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `ionic plugin add cordova-plugin-statusbar` + * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `cordova plugin add org.apache.cordova.statusbar` * @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false. */ fullScreen: function(showFullScreen, showStatusBar) { @@ -2673,7 +2668,7 @@ window.ionic.version = '1.3.3'; * - Works with labels surrounding inputs * - Does not fire off a click if the user moves the pointer too far * - Adds and removes an 'activated' css class - * - Multiple [unit tests](https://github.com/driftyco/ionic/blob/1.x/test/unit/utils/tap.unit.js) for each scenario + * - Multiple [unit tests](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario * */ /* @@ -2884,7 +2879,7 @@ ionic.tap = { if (ele && ele.nodeType === 1) { var element = ele; while (element) { - if (element.getAttribute && element.getAttribute('data-tap-disabled') == 'true') { + if ((element.dataset ? element.dataset.tapDisabled : element.getAttribute && element.getAttribute('data-tap-disabled')) == 'true') { return true; } element = element.parentElement; @@ -2984,7 +2979,7 @@ function tapMouseDown(e) { e.stopPropagation(); if (!ionic.Platform.isEdge() && (!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) && - !isSelectOrOption(e.target.tagName) && !e.target.isContentEditable && !ionic.tap.isVideo(e.target)) { + !isSelectOrOption(e.target.tagName) && !ionic.tap.isVideo(e.target)) { // If you preventDefault on a text input then you cannot move its text caret/cursor. // Allow through only the text input default. However, without preventDefault on an // input the 300ms delay can change focus on inputs after the keyboard shows up. @@ -3108,10 +3103,6 @@ function tapIgnoreEvent(e) { return true; } - if(e.target.tagName == 'SELECT') { - return true; - } - if (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) { e.preventDefault(); return true; @@ -3280,7 +3271,7 @@ ionic.DomUtil.ready(function() { eleToActivate = ele; break; } - if (ele.classList && ele.classList.contains('button')) { + if (ele.classList.contains('button')) { eleToActivate = ele; break; } @@ -7012,8 +7003,6 @@ ionic.scroll = { if(options.startY >= 0 || options.startX >= 0) { ionic.requestAnimationFrame(function() { - self.__originalContainerHeight = self.el.getBoundingClientRect().height; - self.el.scrollTop = options.startY || 0; self.el.scrollLeft = options.startX || 0; @@ -7511,18 +7500,11 @@ ionic.scroll = { }; self.handleTouchMove = function(e) { - if (self.__frozenShut) { + if(self.__frozenShut) { e.preventDefault(); e.stopPropagation(); return false; - - } else if ( self.__frozen ){ - e.preventDefault(); - // let it propagate so other events such as drag events can happen, - // but don't let it actually scroll - return false; } - return true; }; container.addEventListener('scroll', self.onScroll); @@ -7546,14 +7528,15 @@ ionic.scroll = { var self = this; var container = self.__container; + container.removeEventListener('resetScrollView', self.resetScrollView); container.removeEventListener('scroll', self.onScroll); + container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView); + container.removeEventListener('resetScrollView', self.resetScrollView); container.removeEventListener(ionic.EVENTS.touchstart, self.handleTouchMove); container.removeEventListener(ionic.EVENTS.touchmove, self.handleTouchMove); - document.removeEventListener('resetScrollView', self.resetScrollView); - ionic.tap.removeClonedInputs(container, self); delete self.__container; @@ -10761,11 +10744,6 @@ ionic.views.Slider = ionic.views.View.inherit({ s.emit('onTransitionStart', s); if (s.activeIndex !== s.previousIndex) { s.emit('onSlideChangeStart', s); - _scope.$emit("$ionicSlides.slideChangeStart", { - slider: s, - activeIndex: s.getSlideDataIndex(s.activeIndex), - previousIndex: s.getSlideDataIndex(s.previousIndex) - }); if (s.activeIndex > s.previousIndex) { s.emit('onSlideNextStart', s); } @@ -10785,11 +10763,6 @@ ionic.views.Slider = ionic.views.View.inherit({ s.emit('onTransitionEnd', s); if (s.activeIndex !== s.previousIndex) { s.emit('onSlideChangeEnd', s); - _scope.$emit("$ionicSlides.slideChangeEnd", { - slider: s, - activeIndex: s.getSlideDataIndex(s.activeIndex), - previousIndex: s.getSlideDataIndex(s.previousIndex) - }); if (s.activeIndex > s.previousIndex) { s.emit('onSlideNextEnd', s); } @@ -11004,49 +10977,14 @@ ionic.views.Slider = ionic.views.View.inherit({ } s.observers = []; }; - - s.updateLoop = function(){ - var currentSlide = s.slides.eq(s.activeIndex); - if ( angular.element(currentSlide).hasClass(s.params.slideDuplicateClass) ){ - // we're on a duplicate, so slide to the non-duplicate - var swiperSlideIndex = angular.element(currentSlide).attr("data-swiper-slide-index"); - var slides = s.wrapper.children('.' + s.params.slideClass); - for ( var i = 0; i < slides.length; i++ ){ - if ( !angular.element(slides[i]).hasClass(s.params.slideDuplicateClass) && angular.element(slides[i]).attr("data-swiper-slide-index") === swiperSlideIndex ){ - s.slideTo(i, 0, false, true); - break; - } - } - // if we needed to switch slides, we did that. So, now call the createLoop function internally - setTimeout(function(){ - s.createLoop(); - }, 50); - } - } - - s.getSlideDataIndex = function(slideIndex){ - // this is an Ionic custom function - // Swiper loops utilize duplicate DOM elements for slides when in a loop - // which means that we cannot rely on the actual slide index for our events - // because index 0 does not necessarily point to index 0 - // and index n+1 does not necessarily point to the expected piece of data - // therefore, rather than using the actual slide index we should - // use the data index that swiper includes as an attribute on the dom elements - // because this is what will be meaningful to the consumer of our events - var slide = s.slides.eq(slideIndex); - var attributeIndex = angular.element(slide).attr("data-swiper-slide-index"); - return parseInt(attributeIndex); - } - /*========================= Loop ===========================*/ // Create looped slides s.createLoop = function () { - //console.log("Slider create loop method"); - //var toRemove = s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass); - //angular.element(toRemove).remove(); - s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass).remove(); + + var toRemove = s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass); + angular.element(toRemove).remove(); var slides = s.wrapper.children('.' + s.params.slideClass); @@ -11066,17 +11004,16 @@ ionic.views.Slider = ionic.views.View.inherit({ slide.attr('data-swiper-slide-index', index); }); for (i = 0; i < appendSlides.length; i++) { - newNode = angular.element(appendSlides[i]).clone().addClass(s.params.slideDuplicateClass); newNode.removeAttr('ng-transclude'); newNode.removeAttr('ng-repeat'); scope = angular.element(appendSlides[i]).scope(); newNode = $compile(newNode)(scope); angular.element(s.wrapper).append(newNode); - //s.wrapper.append($(appendSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass)); + //s.wrapper.append($(appendSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass)); } for (i = prependSlides.length - 1; i >= 0; i--) { - //s.wrapper.prepend($(prependSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass)); + //s.wrapper.prepend($(prependSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass)); newNode = angular.element(prependSlides[i]).clone().addClass(s.params.slideDuplicateClass); newNode.removeAttr('ng-transclude'); @@ -13374,8 +13311,8 @@ ionic.views.Slider = ionic.views.View.inherit({ */ /** - * @license AngularJS v1.5.3 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.3 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, document, undefined) {'use strict'; @@ -13432,7 +13369,7 @@ function minErr(module, ErrorConstructor) { return match; }); - message += '\nhttp://errors.angularjs.org/1.5.3/' + + message += '\nhttp://errors.angularjs.org/1.4.3/' + (module ? module + '/' : '') + code; for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { @@ -13563,9 +13500,29 @@ 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'; +/** + * @ngdoc function + * @name angular.lowercase + * @module ng + * @kind function + * + * @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 hasOwnProperty = Object.prototype.hasOwnProperty; -var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; +/** + * @ngdoc function + * @name angular.uppercase + * @module ng + * @kind function + * + * @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;}; @@ -13585,7 +13542,7 @@ var manualUppercase = function(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 +// with correct but slower alternatives. if ('i' !== 'I'.toLowerCase()) { lowercase = manualLowercase; uppercase = manualUppercase; @@ -13622,25 +13579,20 @@ msie = document.documentMode; * String ...) */ function isArrayLike(obj) { - - // `null`, `undefined` and `window` are not array-like - if (obj == null || isWindow(obj)) return false; - - // arrays, strings and jQuery/jqLite objects are array like - // * jqLite is either the jQuery or jqLite constructor function - // * we have to check the existence of jqLite first as this method is called - // via the forEach method when constructing the jqLite object in the first place - if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; + if (obj == null || isWindow(obj)) { + return false; + } // 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; - // 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'); + if (obj.nodeType === NODE_TYPE_ELEMENT && length) { + return true; + } + return isString(obj) || isArray(obj) || length === 0 || + typeof length === 'number' && length > 0 && (length - 1) in obj; } /** @@ -13660,7 +13612,7 @@ function isArrayLike(obj) { * * Unlike ES262's * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), - * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just + * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just * return the value provided. * ```js @@ -13737,7 +13689,7 @@ function forEachSorted(obj, iterator, context) { * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) {iteratorFn(key, value);}; + return function(value, key) { iteratorFn(key, value); }; } /** @@ -13783,12 +13735,6 @@ function baseExtend(dst, objs, deep) { if (deep && isObject(src)) { if (isDate(src)) { dst[key] = new Date(src.valueOf()); - } else if (isRegExp(src)) { - dst[key] = new RegExp(src); - } else if (src.nodeName) { - dst[key] = src.cloneNode(true); - } else if (isElement(src)) { - dst[key] = src.clone(); } else { if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; baseExtend(dst[key], [src], true); @@ -13901,10 +13847,10 @@ function identity($) {return $;} identity.$inject = []; -function valueFn(value) {return function valueRef() {return value;};} +function valueFn(value) {return function() {return value;};} function hasCustomToString(obj) { - return isFunction(obj.toString) && obj.toString !== toString; + return isFunction(obj.toString) && obj.toString !== Object.prototype.toString; } @@ -14103,13 +14049,9 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/; function isTypedArray(value) { - return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); -} - -function isArrayBuffer(obj) { - return toString.call(obj) === '[object ArrayBuffer]'; + return TYPED_ARRAY_REGEXP.test(toString.call(value)); } @@ -14149,7 +14091,7 @@ function isElement(node) { * @returns {object} in the form of {key1:true, key2:true, ...} */ function makeMap(str) { - var obj = {}, items = str.split(','), i; + var obj = {}, items = str.split(","), i; for (i = 0; i < items.length; i++) { obj[items[i]] = true; } @@ -14231,141 +14173,98 @@ function arrayRemove(array, value) { </file> </example> */ -function copy(source, destination) { - var stackSource = []; - var stackDest = []; - - if (destination) { - if (isTypedArray(destination) || isArrayBuffer(destination)) { - 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."); - } +function copy(source, destination, stackSource, stackDest) { + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + "Can't copy! Making copies of Window or Scope instances is not supported."); + } + if (isTypedArray(destination)) { + throw ngMinErr('cpta', + "Can't copy! TypedArray destination cannot be mutated."); + } + + if (!destination) { + destination = source; + if (isObject(source)) { + var index; + if (stackSource && (index = stackSource.indexOf(source)) !== -1) { + return stackDest[index]; + } + + // TypedArray, Date and RegExp have specific copy functionality and must be + // pushed onto the stack before returning. + // Array and other objects create the base object and recurse to copy child + // objects. The array/object will be pushed onto the stack when recursed. + if (isArray(source)) { + return copy(source, [], stackSource, stackDest); + } else if (isTypedArray(source)) { + destination = new source.constructor(source); + } else if (isDate(source)) { + destination = new Date(source.getTime()); + } else if (isRegExp(source)) { + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; + } else { + var emptyObject = Object.create(getPrototypeOf(source)); + return copy(source, emptyObject, stackSource, stackDest); + } - // Empty the destination object - if (isArray(destination)) { - destination.length = 0; - } else { - forEach(destination, function(value, key) { - if (key !== '$$hashKey') { - delete destination[key]; - } - }); + if (stackDest) { + stackSource.push(source); + stackDest.push(destination); + } } + } else { + if (source === destination) throw ngMinErr('cpi', + "Can't copy! Source and destination are identical."); - stackSource.push(source); - stackDest.push(destination); - return copyRecurse(source, destination); - } + stackSource = stackSource || []; + stackDest = stackDest || []; - return copyElement(source); + if (isObject(source)) { + stackSource.push(source); + stackDest.push(destination); + } - function copyRecurse(source, destination) { - var h = destination.$$hashKey; - var key; + var result, key; if (isArray(source)) { - for (var i = 0, ii = source.length; i < ii; i++) { - destination.push(copyElement(source[i])); + destination.length = 0; + for (var i = 0; i < source.length; i++) { + destination.push(copy(source[i], null, stackSource, stackDest)); } - } 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]); + } else { + var h = destination.$$hashKey; + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + delete destination[key]; + }); } - } 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]); + if (isBlankObject(source)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in source) { + destination[key] = copy(source[key], null, stackSource, stackDest); } - } - } 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]); + } else if (source && typeof source.hasOwnProperty === 'function') { + // Slow path, which must rely on hasOwnProperty + for (key in source) { + if (source.hasOwnProperty(key)) { + destination[key] = copy(source[key], null, stackSource, stackDest); + } + } + } else { + // Slowest path --- hasOwnProperty can't be called as a method + for (key in source) { + if (hasOwnProperty.call(source, key)) { + destination[key] = copy(source[key], null, stackSource, stackDest); + } } } - } - setHashKey(destination, h); - return destination; - } - - function copyElement(source) { - // Simple values - if (!isObject(source)) { - return source; - } - - // Already copied values - var index = stackSource.indexOf(source); - if (index !== -1) { - return stackDest[index]; - } - - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); - } - - var needsRecurse = false; - var destination = copyType(source); - - if (destination === undefined) { - destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); - needsRecurse = true; - } - - stackSource.push(source); - stackDest.push(destination); - - return needsRecurse - ? copyRecurse(source, destination) - : destination; - } - - function copyType(source) { - switch (toString.call(source)) { - case '[object Int8Array]': - case '[object Int16Array]': - case '[object Int32Array]': - case '[object Float32Array]': - case '[object Float64Array]': - case '[object Uint8Array]': - case '[object Uint8ClampedArray]': - case '[object Uint16Array]': - case '[object Uint32Array]': - return new source.constructor(copyElement(source.buffer)); - - case '[object ArrayBuffer]': - //Support: IE10 - if (!source.slice) { - var copied = new ArrayBuffer(source.byteLength); - new Uint8Array(copied).set(new Uint8Array(source)); - return copied; - } - return source.slice(0); - - case '[object Boolean]': - case '[object Number]': - case '[object String]': - case '[object Date]': - return new source.constructor(source.valueOf()); - - case '[object RegExp]': - var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - re.lastIndex = source.lastIndex; - return re; - - case '[object Blob]': - return new source.constructor([source], {type: source.type}); - } - - if (isFunction(source.cloneNode)) { - return source.cloneNode(true); + setHashKey(destination,h); } } + return destination; } /** @@ -14428,76 +14327,60 @@ function equals(o1, o2) { if (o1 === null || o2 === null) return false; 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 (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for (key = 0; key < length; key++) { + if (t1 == t2) { + if (t1 == 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) == o2.length) { + for (key = 0; key < length; key++) { + if (!equals(o1[key], o2[key])) return false; + } + return true; + } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + return isRegExp(o2) ? o1.toString() == o2.toString() : false; + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + o2[key] !== undefined && + !isFunction(o2[key])) return false; } return true; } - } else if (isDate(o1)) { - if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); - } else if (isRegExp(o1)) { - if (!isRegExp(o2)) return false; - return o1.toString() == o2.toString(); - } else { - if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || - isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = createMap(); - for (key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; - if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for (key in o2) { - if (!(key in keySet) && - key.charAt(0) !== '$' && - isDefined(o2[key]) && - !isFunction(o2[key])) return false; - } - return true; } } return false; } var csp = function() { - if (!isDefined(csp.rules)) { - - - var ngCspElement = (document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); - - if (ngCspElement) { - var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || - ngCspElement.getAttribute('data-ng-csp'); - csp.rules = { - noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1), - noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1) - }; - } else { - csp.rules = { - noUnsafeEval: noUnsafeEval(), - noInlineStyle: false - }; - } - } + if (isDefined(csp.isActive_)) return csp.isActive_; - return csp.rules; + var active = !!(document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); - function noUnsafeEval() { + if (!active) { try { /* jshint -W031, -W054 */ new Function(''); /* jshint +W031, +W054 */ - return false; } catch (e) { - return true; + active = true; } } + + return (csp.isActive_ = active); }; /** @@ -14635,7 +14518,7 @@ function toJsonReplacer(key, value) { * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { - if (isUndefined(obj)) return undefined; + if (typeof obj === 'undefined') return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } @@ -14662,10 +14545,7 @@ function fromJson(json) { } -var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { - // 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; } @@ -14680,9 +14560,8 @@ function addDateMinutes(date, minutes) { function convertTimezoneToLocal(date, timezone, reverse) { reverse = reverse ? -1 : 1; - var dateTimezoneOffset = date.getTimezoneOffset(); - var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); - return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); + var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); } @@ -14701,7 +14580,7 @@ function startingTag(element) { 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); } @@ -14733,19 +14612,13 @@ function tryDecodeURIComponent(value) { * @returns {Object.<string,boolean|Array>} */ function parseKeyValue(/**string*/keyValue) { - var obj = {}; + var obj = {}, key_value, key; forEach((keyValue || "").split('&'), function(keyValue) { - var splitPoint, key, val; if (keyValue) { - key = keyValue = keyValue.replace(/\+/g,'%20'); - splitPoint = keyValue.indexOf('='); - if (splitPoint !== -1) { - key = keyValue.substring(0, splitPoint); - val = keyValue.substring(splitPoint + 1); - } - key = tryDecodeURIComponent(key); + key_value = keyValue.replace(/\+/g,'%20').split('='); + key = tryDecodeURIComponent(key_value[0]); if (isDefined(key)) { - val = isDefined(val) ? tryDecodeURIComponent(val) : true; + var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; if (!hasOwnProperty.call(obj, key)) { obj[key] = val; } else if (isArray(obj[key])) { @@ -14849,17 +14722,10 @@ function getNgAttribute(element, ngAttr) { * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `<body>` or `<html>` tags. * - * There are a few things to keep in mind when using `ngApp`: - * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. - * - AngularJS applications cannot be nested within each other. - * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. - * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and - * {@link ngRoute.ngView `ngView`}. - * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, - * causing animations to stop working and making the injector inaccessible from outside the app. + * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. * * You can specify an **AngularJS module** to be used as the root module for the application. This * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It @@ -14999,25 +14865,16 @@ function angularInit(element, bootstrap) { * @description * Use this function to manually start up angular application. * - * For more information, see the {@link guide/bootstrap Bootstrap guide}. + * See: {@link guide/bootstrap Bootstrap} + * + * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. + * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular 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. - * They must use {@link ng.directive:ngApp ngApp}. - * </div> - * - * <div class="alert alert-warning"> - * **Note:** Do not bootstrap the app on an element with a directive that uses {@link ng.$compile#transclusion transclusion}, - * such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}. - * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, - * causing animations to stop working and making the injector inaccessible from outside the app. - * </div> - * * ```html * <!doctype html> * <html> @@ -15160,6 +15017,7 @@ function snake_case(name, separator) { } var bindJQueryFired = false; +var skipDestroyOnNextJQueryCleanData; function bindJQuery() { var originalCleanData; @@ -15169,9 +15027,10 @@ function bindJQuery() { // bind to jQuery if present; var jqName = jq(); - jQuery = isUndefined(jqName) ? window.jQuery : // use jQuery (if present) - !jqName ? undefined : // use jqLite - window[jqName]; // use jQuery specified by `ngJq` + jQuery = window.jQuery; // use default jQuery. + if (isDefined(jqName)) { // `ngJq` present + jQuery = jqName === null ? undefined : window[jqName]; // if empty; use jqLite. if not empty, 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. @@ -15193,11 +15052,15 @@ function bindJQuery() { 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'); + if (!skipDestroyOnNextJQueryCleanData) { + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, "events"); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } } + } else { + skipDestroyOnNextJQueryCleanData = false; } originalCleanData(elems); }; @@ -15272,24 +15135,22 @@ function getter(obj, path, bindFnToScope) { /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns {Array} the inputted object or a jqLite collection containing the nodes + * @returns {jqLite} jqLite collection containing the nodes */ function getBlockNodes(nodes) { - // TODO(perf): update `nodes` instead of creating a new object? + // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original + // collection, otherwise update the original collection. var node = nodes[0]; var endNode = nodes[nodes.length - 1]; - var blockNodes; + var blockNodes = [node]; - for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { - if (blockNodes || nodes[i] !== node) { - if (!blockNodes) { - blockNodes = jqLite(slice.call(nodes, 0, i)); - } - blockNodes.push(node); - } - } + do { + node = node.nextSibling; + if (!node) break; + blockNodes.push(node); + } while (node !== endNode); - return blockNodes || nodes; + return jqLite(blockNodes); } @@ -15353,8 +15214,8 @@ function setupModuleLoader(window) { * All modules (angular 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}, - * whereas passing more than one argument creates a new {@link angular.Module} + * When passed two or more arguments, a new module is created. If passed only one argument, an + * existing module (the name passed as the first argument to `module`) is retrieved. * * * # Module @@ -15391,7 +15252,7 @@ function setupModuleLoader(window) { * unspecified then the module is being retrieved for further configuration. * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. - * @returns {angular.Module} new module with the {@link angular.Module} api. + * @returns {module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { var assertNotHasOwnProperty = function(name, context) { @@ -15503,7 +15364,7 @@ function setupModuleLoader(window) { * @param {string} name constant name * @param {*} object Constant value. * @description - * Because the constants are fixed, they get applied before other provide methods. + * Because the constant are fixed, they get applied before other provide methods. * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), @@ -15599,19 +15460,6 @@ 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 {Object} options Component definition object (a simplified - * {@link ng.$compile#directive-definition-object directive definition object}) - * - * @description - * See {@link ng.$compileProvider#component $compileProvider.component()}. - */ - component: invokeLaterAndSetModuleName('$compileProvider', 'component'), - - /** - * @ngdoc method * @name angular.Module#config * @module ng * @param {Function} configFn Execute this function on module load. Useful for service @@ -15686,7 +15534,7 @@ function serializeObject(obj) { val = toJsonReplacer(key, val); if (isObject(val)) { - if (seen.indexOf(val) >= 0) return '...'; + if (seen.indexOf(val) >= 0) return '<<already seen>>'; seen.push(val); } @@ -15697,7 +15545,7 @@ function serializeObject(obj) { function toDebugString(obj) { if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (isUndefined(obj)) { + } else if (typeof obj === 'undefined') { return 'undefined'; } else if (typeof obj !== 'string') { return serializeObject(obj); @@ -15708,6 +15556,7 @@ function toDebugString(obj) { /* global angularModule: true, version: true, + $LocaleProvider, $CompileProvider, htmlAnchorDirective, @@ -15724,6 +15573,7 @@ function toDebugString(obj) { ngClassDirective, ngClassEvenDirective, ngClassOddDirective, + ngCspDirective, ngCloakDirective, ngControllerDirective, ngFormDirective, @@ -15760,19 +15610,14 @@ function toDebugString(obj) { $AnchorScrollProvider, $AnimateProvider, - $CoreAnimateCssProvider, - $$CoreAnimateJsProvider, $$CoreAnimateQueueProvider, - $$AnimateRunnerFactoryProvider, - $$AnimateAsyncRunFactoryProvider, + $$CoreAnimateRunnerProvider, $BrowserProvider, $CacheFactoryProvider, $ControllerProvider, - $DateProvider, $DocumentProvider, $ExceptionHandlerProvider, $FilterProvider, - $$ForceReflowProvider, $InterpolateProvider, $IntervalProvider, $$HashMapProvider, @@ -15780,7 +15625,6 @@ function toDebugString(obj) { $HttpParamSerializerProvider, $HttpParamSerializerJQLikeProvider, $HttpBackendProvider, - $xhrFactoryProvider, $LocationProvider, $LogProvider, $ParseProvider, @@ -15807,9 +15651,8 @@ function toDebugString(obj) { * @name angular.version * @module ng * @description - * An object that contains information about the current AngularJS version. - * - * This object has the following properties: + * An object that contains information about the current AngularJS version. This object has the + * following properties: * * - `full` – `{string}` – Full version string, such as "0.9.18". * - `major` – `{number}` – Major version number, such as "0". @@ -15818,11 +15661,11 @@ function toDebugString(obj) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.5.3', // all of these placeholder strings will be replaced by grunt's + full: '1.4.3', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 5, + minor: 4, dot: 3, - codeName: 'diplohaplontic-meiosis' + codeName: 'foam-acceleration' }; @@ -15861,6 +15704,11 @@ function publishExternalAPI(angular) { }); angularModule = setupModuleLoader(window); + try { + angularModule('ngLocale'); + } catch (e) { + angularModule('ngLocale', []).provider('$locale', $LocaleProvider); + } angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { @@ -15923,25 +15771,20 @@ function publishExternalAPI(angular) { $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, - $animateCss: $CoreAnimateCssProvider, - $$animateJs: $$CoreAnimateJsProvider, $$animateQueue: $$CoreAnimateQueueProvider, - $$AnimateRunner: $$AnimateRunnerFactoryProvider, - $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, + $$AnimateRunner: $$CoreAnimateRunnerProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, - $$forceReflow: $$ForceReflowProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, $httpParamSerializer: $HttpParamSerializerProvider, $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, - $xhrFactory: $xhrFactoryProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, @@ -15998,22 +15841,16 @@ 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**. - * - * 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 - * commonly needed functionality with the goal of having a very small footprint. + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." * - * 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-success">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 + * commonly needed functionality with the goal of having a very small footprint.</div> * - * <div class="alert alert-info">**Note:** All element references in Angular 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> + * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. * - * <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> + * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or + * jqLite; they are never raw DOM references.</div> * * ## Angular's jqLite * jqLite provides only the following jQuery methods: @@ -16026,8 +15863,7 @@ function publishExternalAPI(angular) { * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. - * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing. + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'. * - [`data()`](http://api.jquery.com/data/) * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) @@ -16037,7 +15873,7 @@ function publishExternalAPI(angular) { * - [`html()`](http://api.jquery.com/html/) * - [`next()`](http://api.jquery.com/next/) - Does not support selectors * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) @@ -16051,7 +15887,7 @@ function publishExternalAPI(angular) { * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) * - [`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/) - Does not support namespaces * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -16123,10 +15959,10 @@ function camelCase(name) { replace(MOZ_HACK_REGEXP, 'Moz$1'); } -var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; +var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; 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; +var TAG_NAME_REGEXP = /<([\w:]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; var wrapMap = { 'option': [1, '<select multiple="multiple">', '</select>'], @@ -16161,12 +15997,6 @@ 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, fragment = context.createDocumentFragment(), @@ -16219,24 +16049,6 @@ function jqLiteParseHTML(html, context) { return []; } -function jqLiteWrapNode(node, wrapper) { - var parent = node.parentNode; - - if (parent) { - parent.replaceChild(wrapper, node); - } - - wrapper.appendChild(node); -} - - -// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. -var jqLiteContains = Node.prototype.contains || function(arg) { - // jshint bitwise: false - return !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true -}; - ///////////////////////////////////////////// function JQLite(element) { if (element instanceof JQLite) { @@ -16295,23 +16107,17 @@ function jqLiteOff(element, type, fn, unsupported) { delete events[type]; } } else { - - var removeHandler = function(type) { - var listenerFns = events[type]; + forEach(type.split(' '), function(type) { if (isDefined(fn)) { + var listenerFns = events[type]; arrayRemove(listenerFns || [], fn); + if (listenerFns && listenerFns.length > 0) { + return; + } } - if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { - removeEventListenerFn(element, type, handle); - delete events[type]; - } - }; - forEach(type.split(' '), function(type) { - removeHandler(type); - if (MOUSE_EVENT_MAP[type]) { - removeHandler(MOUSE_EVENT_MAP[type]); - } + removeEventListenerFn(element, type, handle); + delete events[type]; }); } } @@ -16452,7 +16258,7 @@ function jqLiteInheritedData(element, name, value) { while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if (isDefined(value = jqLite.data(element, names[i]))) return value; + if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host @@ -16479,7 +16285,7 @@ function jqLiteRemove(element, keepData) { function jqLiteDocumentLoaded(action, win) { win = win || window; if (win.document.readyState === 'complete') { - // Force the action to be run async for consistent behavior + // Force the action to be run async for consistent behaviour // from the action's point of view // i.e. it will definitely not be in a $apply win.setTimeout(action); @@ -16558,15 +16364,15 @@ function getBooleanAttrName(element, name) { return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } -function getAliasedAttrName(name) { - return ALIASED_ATTR[name]; +function getAliasedAttrName(element, name) { + var nodeName = element.nodeName; + return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, removeData: jqLiteRemoveData, - hasData: jqLiteHasData, - cleanData: jqLiteCleanData + hasData: jqLiteHasData }, function(fn, name) { JQLite[name] = fn; }); @@ -16697,7 +16503,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))) { + (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values @@ -16718,7 +16524,7 @@ forEach({ // TODO: do we still need this? var value = fn.$dv; // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount; + var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -16767,9 +16573,6 @@ function createEventHandler(element, events) { return event.immediatePropagationStopped === true; }; - // Some events have special handlers that wrap the real handler - var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper; - // Copy event handlers in case event handlers array is modified during execution. if ((eventFnsLength > 1)) { eventFns = shallowCopy(eventFns); @@ -16777,7 +16580,7 @@ function createEventHandler(element, events) { for (var i = 0; i < eventFnsLength; i++) { if (!event.isImmediatePropagationStopped()) { - handlerWrapper(element, event, eventFns[i]); + eventFns[i].call(element, event); } } }; @@ -16788,22 +16591,6 @@ function createEventHandler(element, events) { return eventHandler; } -function defaultHandlerWrapper(element, event, handler) { - handler.call(element, event); -} - -function specialMouseHandlerWrapper(target, event, handler) { - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - var related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if (!related || (related !== target && !jqLiteContains.call(target, related))) { - handler.call(target, event); - } -} - ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single @@ -16832,28 +16619,35 @@ forEach({ var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; var i = types.length; - var addHandler = function(type, specialHandlerWrapper, noEventListener) { + while (i--) { + type = types[i]; var eventFns = events[type]; if (!eventFns) { - eventFns = events[type] = []; - eventFns.specialHandlerWrapper = specialHandlerWrapper; - if (type !== '$destroy' && !noEventListener) { - addEventListenerFn(element, type, handle); - } - } + events[type] = []; - eventFns.push(fn); - }; + if (type === 'mouseenter' || type === 'mouseleave') { + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 - while (i--) { - type = types[i]; - if (MOUSE_EVENT_MAP[type]) { - addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper); - addHandler(type, undefined, true); - } else { - addHandler(type); + jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { + var target = this, related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if (!related || (related !== target && !target.contains(related))) { + handle(event, type); + } + }); + + } else { + if (type !== '$destroy') { + addEventListenerFn(element, type, handle); + } + } + eventFns = events[type]; } + eventFns.push(fn); } }, @@ -16921,7 +16715,12 @@ forEach({ }, wrap: function(element, wrapNode) { - jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]); + wrapNode = jqLite(wrapNode).eq(0).clone()[0]; + var parent = element.parentNode; + if (parent) { + parent.replaceChild(wrapNode, element); + } + wrapNode.appendChild(element); }, remove: jqLiteRemove, @@ -17199,23 +16998,17 @@ var $$HashMapProvider = [function() { * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var ARROW_ARG = /^([^\(]+?)=>/; -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; +var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); -function extractArgs(fn) { - var fnText = fn.toString().replace(STRIP_COMMENTS, ''), - args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); - return args; -} - function anonFn(fn) { // For anonymous functions, showing at the very least the function signature can help in // debugging. - var args = extractArgs(fn); + var fnText = fn.toString().replace(STRIP_COMMENTS, ''), + args = fnText.match(FN_ARGS); if (args) { return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; } @@ -17224,6 +17017,7 @@ function anonFn(fn) { function annotate(fn, strictDi, name) { var $inject, + fnText, argDecl, last; @@ -17238,7 +17032,8 @@ function annotate(fn, strictDi, name) { throw $injectorMinErr('strictdi', '{0} is not using explicit annotation and cannot be invoked in strict mode', name); } - argDecl = extractArgs(fn); + fnText = fn.toString().replace(STRIP_COMMENTS, ''); + argDecl = fnText.match(FN_ARGS); forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); @@ -17628,20 +17423,8 @@ function annotate(fn, strictDi, name) { * * Register a **service constructor**, which will be invoked with `new` to create the service * instance. - * This is short for registering a service where its provider's `$get` property is a factory - * function that returns an instance instantiated by the injector from the service constructor - * function. - * - * Internally it looks a bit like this: - * - * ``` - * { - * $get: function() { - * return $injector.instantiate(constructor); - * } - * } - * ``` - * + * This is short for registering a service where its provider's `$get` property is the service + * constructor function that will be used to instantiate the service instance. * * You should use {@link auto.$provide#service $provide.service(class)} if you define your service * as a type/class. @@ -17681,13 +17464,14 @@ function annotate(fn, strictDi, name) { * @description * * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its + * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. That also means it is not possible to inject other services into a value service. + * service**. * * 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 Angular + * {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. @@ -17712,11 +17496,8 @@ function annotate(fn, strictDi, name) { * @name $provide#constant * @description * - * Register a **constant service** with the {@link auto.$injector $injector}, such as a string, - * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not - * possible to inject other services into a constant. - * - * But unlike {@link auto.$provide#value value}, a constant can be + * Register a **constant service**, such as a string, a number, an array, an object or a function, + * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it 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}. * @@ -17744,7 +17525,7 @@ function annotate(fn, strictDi, name) { * @description * * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behavior of the + * intercepts the creation of a service, allowing it to override or modify the behaviour of the * service. The object returned by the decorator may be the original service, or a new service * object which replaces or wraps and delegates to the original service. * @@ -17793,19 +17574,14 @@ function createInjector(modulesToLoad, strictDi) { throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, - protoInstanceInjector = + instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(serviceName, caller) { var provider = providerInjector.get(serviceName + providerSuffix, caller); - return instanceInjector.invoke( - provider.$get, provider, undefined, serviceName); - }), - instanceInjector = protoInstanceInjector; + return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); + })); + - providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; - var runBlocks = loadModules(modulesToLoad); - instanceInjector = protoInstanceInjector.get('$injector'); - instanceInjector.strictDi = strictDi; - forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); }); + forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); }); return instanceInjector; @@ -17878,7 +17654,6 @@ function createInjector(modulesToLoad, strictDi) { // Module Loading //////////////////////////////////// function loadModules(modulesToLoad) { - assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; @@ -17955,66 +17730,47 @@ function createInjector(modulesToLoad, strictDi) { } } + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } - function injectionArgs(fn, locals, serviceName) { var args = [], - $inject = createInjector.$$annotate(fn, strictDi, serviceName); + $inject = createInjector.$$annotate(fn, strictDi, serviceName), + length, i, + key; - for (var i = 0, length = $inject.length; i < length; i++) { - var key = $inject[i]; + for (i = 0, length = $inject.length; i < length; i++) { + key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } - args.push(locals && locals.hasOwnProperty(key) ? locals[key] : - getService(key, serviceName)); - } - return args; - } - - function isClass(func) { - // IE 9-11 do not support classes and IE9 leaks with the code below. - if (msie <= 11) { - return false; - } - // Workaround for MS Edge. - // Check https://connect.microsoft.com/IE/Feedback/Details/2211653 - return typeof func === 'function' - && /^(?:class\s|constructor\()/.test(Function.prototype.toString.call(func)); - } - - function invoke(fn, self, locals, serviceName) { - if (typeof locals === 'string') { - serviceName = locals; - locals = null; + args.push( + locals && locals.hasOwnProperty(key) + ? locals[key] + : getService(key, serviceName) + ); } - - var args = injectionArgs(fn, locals, serviceName); if (isArray(fn)) { - fn = fn[fn.length - 1]; + fn = fn[length]; } - if (!isClass(fn)) { - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); - } else { - args.unshift(null); - return new (Function.prototype.bind.apply(fn, args))(); - } + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); } - function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - var ctor = (isArray(Type) ? Type[Type.length - 1] : Type); - var args = injectionArgs(Type, locals, serviceName); - // Empty object at position 0 is ignored for invocation with `new`, but required. - args.unshift(null); - return new (Function.prototype.bind.apply(ctor, args))(); - } + // Object creation: http://jsperf.com/create-constructor/2 + var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null); + var returnedValue = invoke(Type, instance, locals, serviceName); + return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; + } return { invoke: invoke, @@ -18071,7 +17827,7 @@ function $AnchorScrollProvider() { * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified * in the - * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document). + * [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). * * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to * match any anchor whenever it changes. This can be disabled by calling @@ -18354,8 +18110,27 @@ function prepareAnimateOptions(options) { : {}; } -var $$CoreAnimateJsProvider = function() { - this.$get = noop; +var $$CoreAnimateRunnerProvider = function() { + this.$get = ['$q', '$$rAF', function($q, $$rAF) { + function AnimateRunner() {} + AnimateRunner.all = noop; + AnimateRunner.chain = noop; + AnimateRunner.prototype = { + end: noop, + cancel: noop, + resume: noop, + pause: noop, + complete: noop, + then: function(pass, fail) { + return $q(function(resolve) { + $$rAF(function() { + resolve(); + }); + }).then(pass, fail); + } + }; + return AnimateRunner; + }]; }; // this is prefixed with Core since it conflicts with @@ -18383,75 +18158,65 @@ var $$CoreAnimateQueueProvider = function() { addRemoveClassesPostDigest(element, options.addClass, options.removeClass); } - var runner = new $$AnimateRunner(); // jshint ignore:line - - // since there are no animations to run the runner needs to be - // notified that the animation call is complete. - runner.complete(); - return runner; + return new $$AnimateRunner(); // jshint ignore:line } }; + function addRemoveClassesPostDigest(element, add, remove) { + var data = postDigestQueue.get(element); + var classVal; + + if (!data) { + postDigestQueue.put(element, data = {}); + postDigestElements.push(element); + } - function updateData(data, classes, value) { - var changed = false; - if (classes) { - classes = isString(classes) ? classes.split(' ') : - isArray(classes) ? classes : []; - forEach(classes, function(className) { + if (add) { + forEach(add.split(' '), function(className) { if (className) { - changed = true; - data[className] = value; + data[className] = true; } }); } - return changed; - } - - function handleCSSClassChanges() { - forEach(postDigestElements, function(element) { - var data = postDigestQueue.get(element); - if (data) { - var existing = splitClasses(element.attr('class')); - var toAdd = ''; - var toRemove = ''; - forEach(data, function(status, className) { - var hasClass = !!existing[className]; - if (status !== hasClass) { - if (status) { - toAdd += (toAdd.length ? ' ' : '') + className; - } else { - toRemove += (toRemove.length ? ' ' : '') + className; - } - } - }); - - forEach(element, function(elm) { - toAdd && jqLiteAddClass(elm, toAdd); - toRemove && jqLiteRemoveClass(elm, toRemove); - }); - postDigestQueue.remove(element); - } - }); - postDigestElements.length = 0; - } - - function addRemoveClassesPostDigest(element, add, remove) { - var data = postDigestQueue.get(element) || {}; + if (remove) { + forEach(remove.split(' '), function(className) { + if (className) { + data[className] = false; + } + }); + } - var classesAdded = updateData(data, add, true); - var classesRemoved = updateData(data, remove, false); + if (postDigestElements.length > 1) return; - if (classesAdded || classesRemoved) { + $rootScope.$$postDigest(function() { + forEach(postDigestElements, function(element) { + var data = postDigestQueue.get(element); + if (data) { + var existing = splitClasses(element.attr('class')); + var toAdd = ''; + var toRemove = ''; + forEach(data, function(status, className) { + var hasClass = !!existing[className]; + if (status !== hasClass) { + if (status) { + toAdd += (toAdd.length ? ' ' : '') + className; + } else { + toRemove += (toRemove.length ? ' ' : '') + className; + } + } + }); - postDigestQueue.put(element, data); - postDigestElements.push(element); + forEach(element, function(elm) { + toAdd && jqLiteAddClass(elm, toAdd); + toRemove && jqLiteRemoveClass(elm, toRemove); + }); + postDigestQueue.remove(element); + } + }); - if (postDigestElements.length === 1) { - $rootScope.$$postDigest(handleCSSClassChanges); - } - } + postDigestElements.length = 0; + }); } }]; }; @@ -18572,7 +18337,7 @@ var $AnimateProvider = ['$provide', function($provide) { * when an animation is detected (and animations are enabled), $animate will do the heavy lifting * to ensure that animation runs with the triggered DOM operation. * - * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't + * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't * included and only when it is active then the animation hooks that `$animate` triggers will be * functional. Once active then all structural `ng-` directives will trigger animations as they perform * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`, @@ -18630,8 +18395,8 @@ var $AnimateProvider = ['$provide', function($provide) { * // remove all the animation event listeners listening for `enter` on the given element and its children * $animate.off('enter', container); * - * // remove the event listener function provided by `callback` that is set - * // to listen for `enter` on the given `container` as well as its children + * // remove the event listener function provided by `listenerFn` that is set + * // to listen for `enter` on the given `element` as well as its children * $animate.off('enter', container, callback); * ``` * @@ -18853,30 +18618,17 @@ var $AnimateProvider = ['$provide', function($provide) { * @kind function * * @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 - * `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` - * method (or as part of the `options` parameter): - * - * ```js - * ngModule.animation('.my-inline-animation', function() { - * return { - * animate : function(element, from, to, done, options) { - * //animation - * done(); - * } - * } - * }); - * ``` + * 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 className then the provided from and + * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles + * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter). * * @param {DOMElement} element the element which the CSS styles will be applied to * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation. * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation. * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element. - * (Note that if no animation is detected then this value will not be applied to the element.) + * (Note that if no animation is detected then this value will not be appplied to the element.) * @param {object=} options an optional collection of options/styles that will be applied to the element * * @return {Promise} the animation callback promise @@ -18894,261 +18646,15 @@ var $AnimateProvider = ['$provide', function($provide) { }]; }]; -var $$AnimateAsyncRunFactoryProvider = function() { - this.$get = ['$$rAF', function($$rAF) { - var waitQueue = []; - - function waitForTick(fn) { - waitQueue.push(fn); - if (waitQueue.length > 1) return; - $$rAF(function() { - for (var i = 0; i < waitQueue.length; i++) { - waitQueue[i](); - } - waitQueue = []; - }); - } - - return function() { - var passed = false; - waitForTick(function() { - passed = true; - }); - return function(callback) { - passed ? callback() : waitForTick(callback); - }; - }; - }]; -}; - -var $$AnimateRunnerFactoryProvider = function() { - this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', - function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { - - var INITIAL_STATE = 0; - var DONE_PENDING_STATE = 1; - var DONE_COMPLETE_STATE = 2; - - AnimateRunner.chain = function(chain, callback) { - var index = 0; - - next(); - function next() { - if (index === chain.length) { - callback(true); - return; - } - - chain[index](function(response) { - if (response === false) { - callback(false); - return; - } - index++; - next(); - }); - } - }; - - AnimateRunner.all = function(runners, callback) { - var count = 0; - var status = true; - forEach(runners, function(runner) { - runner.done(onProgress); - }); - - function onProgress(response) { - status = status && response; - if (++count === runners.length) { - callback(status); - } - } - }; - - function AnimateRunner(host) { - this.setHost(host); - - var rafTick = $$animateAsyncRun(); - var timeoutTick = function(fn) { - $timeout(fn, 0, false); - }; - - 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) { - timeoutTick(fn); - } else { - rafTick(fn); - } - }; - this._state = 0; - } - - AnimateRunner.prototype = { - setHost: function(host) { - this.host = host || {}; - }, - - done: function(fn) { - if (this._state === DONE_COMPLETE_STATE) { - fn(); - } else { - this._doneCallbacks.push(fn); - } - }, - - progress: noop, - - getPromise: function() { - if (!this.promise) { - var self = this; - this.promise = $q(function(resolve, reject) { - self.done(function(status) { - status === false ? reject() : resolve(); - }); - }); - } - return this.promise; - }, - - then: function(resolveHandler, rejectHandler) { - return this.getPromise().then(resolveHandler, rejectHandler); - }, - - 'catch': function(handler) { - return this.getPromise()['catch'](handler); - }, - - 'finally': function(handler) { - return this.getPromise()['finally'](handler); - }, - - pause: function() { - if (this.host.pause) { - this.host.pause(); - } - }, - - resume: function() { - if (this.host.resume) { - this.host.resume(); - } - }, - - end: function() { - if (this.host.end) { - this.host.end(); - } - this._resolve(true); - }, - - cancel: function() { - if (this.host.cancel) { - this.host.cancel(); - } - this._resolve(false); - }, - - complete: function(response) { - var self = this; - if (self._state === INITIAL_STATE) { - self._state = DONE_PENDING_STATE; - self._tick(function() { - self._resolve(response); - }); - } - }, - - _resolve: function(response) { - if (this._state !== DONE_COMPLETE_STATE) { - forEach(this._doneCallbacks, function(fn) { - fn(response); - }); - this._doneCallbacks.length = 0; - this._state = DONE_COMPLETE_STATE; - } - } - }; - - return AnimateRunner; - }]; -}; - -/** - * @ngdoc service - * @name $animateCss - * @kind object - * - * @description - * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, - * then the `$animateCss` service will actually perform animations. - * - * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. - */ -var $CoreAnimateCssProvider = function() { - this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { - - return function(element, initialOptions) { - // all of the animation functions should create - // a copy of the options data, however, if a - // parent service has already created a copy then - // we should stick to using that - var options = initialOptions || {}; - if (!options.$$prepared) { - options = copy(options); - } - - // there is no point in applying the styles since - // there is no animation that goes on at all in - // this version of $animateCss. - if (options.cleanupStyles) { - options.from = options.to = null; - } - - if (options.from) { - element.css(options.from); - options.from = null; - } - - /* jshint newcap: false */ - var closed, runner = new $$AnimateRunner(); - return { - start: run, - end: run +function $$AsyncCallbackProvider() { + this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { + return $$rAF.supported + ? function(fn) { return $$rAF(fn); } + : function(fn) { + return $timeout(fn, 0, false); }; - - function run() { - $$rAF(function() { - applyAnimationContents(); - if (!closed) { - runner.complete(); - } - closed = true; - }); - return runner; - } - - function applyAnimationContents() { - if (options.addClass) { - element.addClass(options.addClass); - options.addClass = null; - } - if (options.removeClass) { - element.removeClass(options.removeClass); - options.removeClass = null; - } - if (options.to) { - element.css(options.to); - options.to = null; - } - } - }; }]; -}; +} /* global stripHash: true */ @@ -19175,6 +18681,7 @@ var $CoreAnimateCssProvider = function() { */ function Browser(window, document, $log, $sniffer) { var self = this, + rawDocument = document[0], location = window.location, history = window.history, setTimeout = window.setTimeout, @@ -19237,14 +18744,7 @@ function Browser(window, document, $log, $sniffer) { 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). - } - }; + reloadLocation = null; cacheState(); lastHistoryState = cachedState; @@ -19304,8 +18804,8 @@ function Browser(window, document, $log, $sniffer) { // Do the assignment again so that those two variables are referentially identical. lastHistoryState = cachedState; } else { - if (!sameBase || pendingLocation) { - pendingLocation = url; + if (!sameBase || reloadLocation) { + reloadLocation = url; } if (replace) { location.replace(url); @@ -19314,18 +18814,14 @@ function Browser(window, document, $log, $sniffer) { } else { location.hash = getHash(url); } - if (location.href !== url) { - 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). + // - reloadLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return pendingLocation || location.href.replace(/%27/g,"'"); + return reloadLocation || location.href.replace(/%27/g,"'"); } }; @@ -19347,11 +18843,18 @@ function Browser(window, document, $log, $sniffer) { urlChangeInit = false; function cacheStateAndFireUrlChange() { - pendingLocation = null; cacheState(); fireUrlChange(); } + function getCurrentState() { + try { + return history.state; + } catch (e) { + // MSIE can reportedly throw when there is no state (UNCONFIRMED). + } + } + // This variable should be used *only* inside the cacheState function. var lastCachedState = null; function cacheState() { @@ -19575,10 +19078,10 @@ function $BrowserProvider() { $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - if (angular.isUndefined($scope.cache.get(key))) { + if ($scope.cache.get(key) === undefined) { $scope.keys.push(key); } - $scope.cache.put(key, angular.isUndefined(value) ? null : value); + $scope.cache.put(key, value === undefined ? null : value); }; }]); </file> @@ -19601,9 +19104,9 @@ function $CacheFactoryProvider() { var size = 0, stats = extend({}, options, {id: cacheId}), - data = createMap(), + data = {}, capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = createMap(), + lruHash = {}, freshEnd = null, staleEnd = null; @@ -19731,8 +19234,6 @@ function $CacheFactoryProvider() { delete lruHash[key]; } - if (!(key in data)) return; - delete data[key]; size--; }, @@ -19747,9 +19248,9 @@ function $CacheFactoryProvider() { * Clears the cache object of any entries. */ removeAll: function() { - data = createMap(); + data = {}; size = 0; - lruHash = createMap(); + lruHash = {}; freshEnd = staleEnd = null; }, @@ -20038,7 +19539,7 @@ function $TemplateCacheProvider() { * 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 + * which are not strictly behavioural (such as {@link ngClick}), and which * do not manipulate or replace child nodes (such as {@link ngInclude}). * * #### `priority` @@ -20056,129 +19557,59 @@ function $TemplateCacheProvider() { * 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. + * **If set to `true`,** then a new scope will be created for this directive. 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. + * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from + * normal scope in that it does not prototypically inherit from the 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: + * The 'isolate' scope takes an object hash which defines a set of local scope properties + * derived from the parent scope. These local properties are useful for aliasing values for + * templates. Locals definition is a hash of local scope property to its source: * * * `@` 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 + * 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 `<widget my-attr="hello {{name}}">` and widget definition + * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect + * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the + * `localName` property on the widget scope. The `name` is read from the parent scope (not + * component scope). + * + * * `=` or `=attr` - set up bi-directional binding between a local scope property and the + * parent scope property of name defined via the value of the `attr` attribute. If no `attr` + * name is specified then the attribute name is assumed to be the same as the local name. + * Given `<widget my-attr="parentModel">` and widget definition of + * `scope: { localModel:'=myAttr' }`, then widget 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. + * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent + * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You + * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If + * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use + * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). + * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the + * local name. Given `<widget my-attr="count = count + value">` and widget definition of + * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to + * a function wrapper for the `count = count + value` expression. Often it's desirable to + * pass data from the isolated scope via an expression 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})`. * * * #### `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`. - * + * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will + * allow a component to have its properties bound to the controller, rather than to scope. When the controller + * is instantiated, the initial values of the isolate scope bindings are already available. * * #### `controller` * Controller constructor function. The controller is instantiated before the - * pre-linking phase and can be accessed by other directives (see + * pre-linking phase and it is shared with 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: * @@ -20186,10 +19617,10 @@ function $TemplateCacheProvider() { * * `$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): + * `function([scope], cloneLinkingFn, futureParentElement)`. + * * `scope`: optional argument to override the scope. + * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. + * * `futureParentElement`: * * 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) @@ -20197,48 +19628,14 @@ function $TemplateCacheProvider() { * 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). - * - * The controller can provide the following methods that act as life-cycle hooks: - * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and - * had their bindings initialized (and before the pre & 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: ... }`. Use this hook to trigger updates within a component - * such as cloning the bound value to prevent accidental mutation of the outer value. - * * `$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. * * * #### `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. - * 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: + * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the + * injected argument will be an array in corresponding order. If no such directive can be + * found, or if the directive does not have a controller, then an error is raised (unless no link function + * is specified, 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. @@ -20252,10 +19649,9 @@ function $TemplateCacheProvider() { * * #### `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. + * This allows the controller to be referenced from the directive template. The directive + * needs to define a scope for this configuration to be used. Useful in the case when + * directive is used as component. * * * #### `restrict` @@ -20331,6 +19727,14 @@ function $TemplateCacheProvider() { * The contents are compiled and provided to the directive as a **transclusion function**. See the * {@link $compile#transclusion Transclusion} section below. * + * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element or the entire element: + * + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. + * * * #### `compile` * @@ -20358,7 +19762,7 @@ function $TemplateCacheProvider() { * <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 + * own templates or compile functions. Compiling these directives results in an infinite loop and a * stack overflow errors. * * This can be avoided by manually using $compile in the postLink function to imperatively compile @@ -20414,7 +19818,7 @@ function $TemplateCacheProvider() { * 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. + * like any other controller. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. * This is the same as the `$transclude` @@ -20440,7 +19844,7 @@ function $TemplateCacheProvider() { * * ### Transclusion * - * Transclusion is the process of extracting a collection of DOM elements from one part of the DOM and + * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and * copying them to another part of the DOM, while maintaining their connection to the original AngularJS * scope from where they were taken. * @@ -20460,34 +19864,6 @@ function $TemplateCacheProvider() { * 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 @@ -20508,7 +19884,7 @@ function $TemplateCacheProvider() { * 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 + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. * </div> * @@ -20540,7 +19916,7 @@ function $TemplateCacheProvider() { * </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 + * automatically destroy their transluded clones as necessary so you do not need to worry about this if * you are simply using {@link ngTransclude} to inject the transclusion into your directive. * * @@ -20565,19 +19941,19 @@ function $TemplateCacheProvider() { * * The `$parent` scope hierarchy will look like this: * - ``` - - $rootScope - - isolate - - transclusion - ``` + * ``` + * - $rootScope + * - isolate + * - transclusion + * ``` * * but the scopes will inherit prototypically from different scopes to their `$parent`. * - ``` - - $rootScope - - transclusion - - isolate - ``` + * ``` + * - $rootScope + * - transclusion + * - isolate + * ``` * * * ### Attributes @@ -20585,9 +19961,10 @@ function $TemplateCacheProvider() { * 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. + * 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 @@ -20708,15 +20085,8 @@ function $TemplateCacheProvider() { * 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 - * } - * } - * ``` + * to controller instances; if given, it will make the controllers + * available to directives. * * `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. @@ -20775,18 +20145,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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 LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; var bindings = {}; forEach(scope, function(definition, scopeName) { - if (definition in bindingCache) { - bindings[scopeName] = bindingCache[definition]; - return; - } var match = definition.match(LOCAL_REGEXP); if (!match) { @@ -20804,9 +20169,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { optional: match[3] === '?', attrName: match[4] || scopeName }; - if (match[4]) { - bindingCache[definition] = bindings[scopeName]; - } }); return bindings; @@ -20852,11 +20214,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + throw $compileMinErr('baddir', "Directive 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", + "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", name); } } @@ -20872,11 +20234,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @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. + * @param {Function|Array} directiveFactory An injectable directive factory function. See + * {@link guide/directive} for more info. * @returns {ng.$compileProvider} Self for chaining. */ - this.directive = function registerDirective(name, directiveFactory) { + this.directive = function registerDirective(name, directiveFactory) { assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -20899,6 +20261,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); directive.restrict = directive.restrict || 'EA'; + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; + } directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -20915,131 +20282,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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. - * - `$...` – 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 || noop; - - 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); - return { - 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 any annotation properties (starting with $) over to the factory function - // These could be used by libraries such as the new component router - forEach(options, function(val, key) { - if (key.charAt(0) === '$') { - factory[key] = val; - controller[key] = val; - } - }); - - factory.$inject = ['$injector']; - - return this.directive(name, factory); - }; - /** * @ngdoc method @@ -21131,75 +20373,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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', + '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $sce, $animate, $$sanitizeUri) { - - var SIMPLE_ATTR_NAME = /^\w/; - var specialAttrHolder = 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() { - for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { - onChangesQueue[i](); - } - // Reset the queue to trigger a new schedule next time there is a change - onChangesQueue = undefined; - }); - } finally { - onChangesTtl++; - } - } + $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - - function Attributes(element, attributesToCopy) { + var Attributes = function(element, attributesToCopy) { if (attributesToCopy) { var keys = Object.keys(attributesToCopy); var i, l, key; @@ -21213,7 +20393,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } this.$$element = element; - } + }; Attributes.prototype = { /** @@ -21308,7 +20488,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), - aliasedKey = getAliasedAttrName(key), + aliasedKey = getAliasedAttrName(node, key), observer = key, nodeName; @@ -21334,7 +20514,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) || + if ((nodeName === 'a' && key === 'href') || (nodeName === 'img' && key === 'src')) { // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); @@ -21375,14 +20555,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (writeAttr !== false) { - if (value === null || isUndefined(value)) { + if (value === null || value === undefined) { this.$$element.removeAttr(attrName); } else { - if (SIMPLE_ATTR_NAME.test(attrName)) { - this.$$element.attr(attrName, value); - } else { - setSpecialAttr(this.$$element[0], attrName, value); - } + this.$$element.attr(attrName, value); } } @@ -21413,8 +20589,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation - * guide} for more info. + * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { @@ -21424,7 +20599,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { + if (!listeners.$$inter && attrs.hasOwnProperty(key)) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } @@ -21436,18 +20611,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; - 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 { @@ -21461,13 +20624,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' && 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') || []; @@ -21494,14 +20656,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); } : noop; - compile.$$createComment = function(directiveName, comment) { - var content = ''; - if (debugInfoEnabled) { - content = ' ' + (directiveName || '') + ': ' + (comment || '') + ' '; - } - return document.createComment(content); - }; - return compile; //================================ @@ -21513,19 +20667,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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] = document.createElement('span')); + forEach($compileNodes, function(node, index) { + if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { + $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; } - } - + }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); @@ -21534,14 +20682,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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, @@ -21596,7 +20736,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!node) { return 'html'; } else { - return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; + return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; } } @@ -21687,6 +20827,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (nodeLinkFn.scope) { childScope = scope.$new(); compile.$$addScopeInfo(jqLite(node), childScope); + var destroyBindings = nodeLinkFn.$$destroyBindings; + if (destroyBindings) { + nodeLinkFn.$$destroyBindings = null; + childScope.$on('$destroyed', destroyBindings); + } } else { childScope = scope; } @@ -21705,7 +20850,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn = null; } - nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn, + nodeLinkFn); } else if (childLinkFn) { childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); @@ -21715,7 +20861,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { - function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { + + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); @@ -21727,18 +20874,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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; } @@ -21784,11 +20920,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); } - 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); + var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + if (directiveIsMultiElement(directiveNName)) { + if (ngAttrName === directiveNName + 'Start') { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } } nName = directiveNormalize(name.toLowerCase()); @@ -21892,42 +21030,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { + return function(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. @@ -21971,8 +21080,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, linkFn, - didScanForMultipleTransclusion = false, - mightHaveMultipleTransclusionError = false, directiveValue; // executes all directives on the current element @@ -22015,27 +21122,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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(); @@ -22060,22 +21146,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { terminalPriority = directive.priority; $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); + jqLite(document.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, + childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers @@ -22087,69 +21163,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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; + childTranscludeFn = compile($template, transcludeFn); } } @@ -22191,11 +21207,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + if (newIsolateScopeDirective) { + markDirectivesAsIsolate(templateDirectives); } directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); @@ -22277,9 +21290,81 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, - attrs, removeScopeBindingWatches, removeControllerBindingWatches; + + 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); + } + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { + 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, + // but jQuery .data doesn't support attaching data to comment nodes as it's hard to + // clean up (http://bugs.jquery.com/ticket/8335). + // 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; + if (!hasElementTranscludeDirective) { + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + } + return elementControllers; + } + + function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn, + thisLinkFn) { + var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, + attrs; if (compileNode === linkNode) { attrs = templateAttrs; @@ -22289,11 +21374,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrs = new Attributes($element, templateAttrs); } - controllerScope = scope; if (newIsolateScopeDirective) { isolateScope = scope.$new(true); - } else if (newScopeDirective) { - controllerScope = scope.$parent; } if (boundTranscludeFn) { @@ -22301,14 +21383,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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); + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); } if (newIsolateScopeDirective) { @@ -22318,58 +21396,45 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile.$$addScopeClass($element, true); isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings; - removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope, - isolateScope.$$isolateBindings, - newIsolateScopeDirective); - if (removeScopeBindingWatches) { - isolateScope.$on('$destroy', removeScopeBindingWatches); - } - } - - // 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) { - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } - - 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); - removeControllerBindingWatches && removeControllerBindingWatches(); - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + initializeDirectiveBindings(scope, attrs, isolateScope, + isolateScope.$$isolateBindings, + newIsolateScopeDirective, isolateScope); + } + if (elementControllers) { + // Initialize bindToController bindings for new/isolate scopes + var scopeDirective = newIsolateScopeDirective || newScopeDirective; + var bindings; + var controllerForBindings; + if (scopeDirective && elementControllers[scopeDirective.name]) { + bindings = scopeDirective.$$bindings.bindToController; + controller = elementControllers[scopeDirective.name]; + + if (controller && controller.identifier && bindings) { + controllerForBindings = controller; + thisLinkFn.$$destroyBindings = + initializeDirectiveBindings(scope, attrs, controller.instance, + bindings, scopeDirective); + } + } + for (i in elementControllers) { + controller = elementControllers[i]; + var controllerResult = controller(); + + if (controllerResult !== controller.instance) { + // If the controller constructor has a return value, overwrite the instance + // from setupControllers and update the element data + controller.instance = controllerResult; + $element.data('$' + i + 'Controller', controllerResult); + if (controller === controllerForBindings) { + // Remove and re-install bindToController bindings + thisLinkFn.$$destroyBindings(); + thisLinkFn.$$destroyBindings = + initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective); + } + } } } - // 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.$onInit)) { - controllerInstance.$onInit(); - } - if (isFunction(controllerInstance.$onDestroy)) { - controllerScope.$on('$destroy', function callOnDestroyHook() { - controllerInstance.$onDestroy(); - }); - } - }); - // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { linkFn = preLinkFns[i]; @@ -22403,21 +21468,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { ); } - // 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) { + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { var transcludeControllers; + // No scope passed in: if (!isScope(scope)) { - slotName = futureParentElement; futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; @@ -22429,108 +21486,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } - } 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) { + function markDirectivesAsIsolate(directives) { + // mark all directives as needing isolate scope. for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope}); + directives[j] = inherit(directives[j], {$$isolateScope: true}); } } @@ -22557,18 +21521,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { i = 0, ii = directives.length; i < ii; i++) { try { directive = directives[i]; - if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + if ((maxPriority === undefined || 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; } @@ -22684,9 +21641,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + markDirectivesAsIsolate(templateDirectives); } directives = templateDirectives.concat(directives); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); @@ -22735,7 +21690,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn = boundTranscludeFn; } afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, - childBoundTranscludeFn); + childBoundTranscludeFn, afterTemplateNodeLinkFn); } linkQueue = null; }); @@ -22752,7 +21707,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (afterTemplateNodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn, + afterTemplateNodeLinkFn); } }; } @@ -22861,7 +21817,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile: function() { return { pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = createMap())); + var $$observers = (attr.$$observers || (attr.$$observers = {})); if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { throw $compileMinErr('nodomevents', @@ -22956,33 +21912,41 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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 + // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? var fragment = document.createDocumentFragment(); - for (i = 0; i < removeCount; i++) { - fragment.appendChild(elementsToRemove[i]); - } + fragment.appendChild(firstElementToRemove); 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)); + jqLite(newNode).data(jqLite(firstElementToRemove).data()); - // Remove $destroy event listeners from `firstElementToRemove` - jqLite(firstElementToRemove).off('$destroy'); + // Remove data of the replaced element. We cannot just call .remove() + // on the element it since that would deallocate scope that is needed + // for the new node. Instead, remove the data "manually". + if (!jQuery) { + delete jqLite.cache[firstElementToRemove[jqLite.expando]]; + } else { + // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after + // the replaced element. The cleanData version monkey-patched by Angular would cause + // the scope to be trashed and we do need the very same scope to work with the new + // element. However, we cannot just cache the non-patched version and use it here as + // that would break if another library patches the method after Angular does (one + // example is jQuery UI). Instead, set a flag indicating scope destroying should be + // skipped this one time. + skipDestroyOnNextJQueryCleanData = true; + jQuery.cleanData([firstElementToRemove]); + } } - // 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]; + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { + var element = elementsToRemove[k]; + jqLite(element).remove(); // must do this way to clean up expando + fragment.appendChild(element); + delete elementsToRemove[k]; } + elementsToRemove[0] = newNode; elementsToRemove.length = 1; } @@ -23004,61 +21968,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // 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 changes; - forEach(bindings, function initializeBinding(definition, scopeName) { + function initializeDirectiveBindings(scope, attrs, destination, bindings, + directive, newScope) { + var onNewScopeDestroyed; + forEach(bindings, function(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, mode = definition.mode, // @, =, or & lastValue, - parentGet, parentSet, compare, removeWatch; + parentGet, parentSet, compare; + + if (!hasOwnProperty.call(attrs, attrName)) { + // In the case of user defined a binding with the same name as a method in Object.prototype but didn't set + // the corresponding attribute. We need to make sure subsequent code won't access to the prototype function + attrs[attrName] = undefined; + } switch (mode) { case '@': - if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; + if (!attrs[attrName] && !optional) { + destination[scopeName] = undefined; } + attrs.$observe(attrName, function(value) { - if (isString(value)) { - var oldValue = destination[scopeName]; - recordChanges(scopeName, value, oldValue); - destination[scopeName] = value; - } + destination[scopeName] = value; }); attrs.$$observers[attrName].$$scope = scope; - lastValue = attrs[attrName]; - if (isString(lastValue)) { + if (attrs[attrName]) { // 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; + destination[scopeName] = $interpolate(attrs[attrName])(scope); } break; case '=': - if (!hasOwnProperty.call(attrs, attrName)) { - if (optional) break; - attrs[attrName] = void 0; + if (optional && !attrs[attrName]) { + return; } - 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); }; + compare = function(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); + "Expression '{0}' used with directive '{1}' is non-assignable!", + attrs[attrName], directive.name); }; lastValue = destination[scopeName] = parentGet(scope); var parentValueWatch = function parentValueWatch(parentValue) { @@ -23075,37 +22035,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return lastValue = parentValue; }; parentValueWatch.$stateful = true; + var unwatch; if (definition.collection) { - removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); + unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { - removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); + unwatch = 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]); - - destination[scopeName] = parentGet(scope); - - removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) { - var oldValue = destination[scopeName]; - recordChanges(scopeName, newParentValue, oldValue); - destination[scopeName] = newParentValue; - }, parentGet.literal); - - removeWatchCollection.push(removeWatch); + onNewScopeDestroyed = (onNewScopeDestroyed || []); + onNewScopeDestroyed.push(unwatch); break; case '&': - // Don't assign Object.prototype method to scope - parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; + parentGet = $parse(attrs[attrName]); // Don't assign noop to destination if expression is not valid if (parentGet === noop && optional) break; @@ -23116,39 +22057,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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] = {previousValue: previousValue, currentValue: currentValue}; + var destroyBindings = onNewScopeDestroyed ? function destroyBindings() { + for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) { + onNewScopeDestroyed[i](); } + } : noop; + if (newScope && destroyBindings !== noop) { + newScope.$on('$destroy', destroyBindings); + return noop; } - - function triggerOnChangesHook() { - destination.$onChanges(changes); - // Now clear the changes so that we schedule onChanges when more changes arrive - changes = undefined; - } - - return removeWatchCollection.length && function removeWatches() { - for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { - removeWatchCollection[i](); - } - }; + return destroyBindings; } }]; } @@ -23258,7 +22176,7 @@ function removeComments(jqNodes) { var $controllerMinErr = minErr('$controller'); -var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; +var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; function identifierForController(controller, ident) { if (ident && isString(ident)) return ident; if (isString(controller)) { @@ -23284,15 +22202,6 @@ function $ControllerProvider() { /** * @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. @@ -23347,7 +22256,7 @@ function $ControllerProvider() { * 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) { + return function(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 @@ -23398,7 +22307,7 @@ function $ControllerProvider() { } var instantiate; - return instantiate = extend(function $controllerInit() { + return instantiate = extend(function() { var result = $injector.invoke(expression, instance, locals, constructor); if (result !== instance && (isObject(result) || isFunction(result))) { instance = result; @@ -23514,29 +22423,6 @@ function $ExceptionHandlerProvider() { }]; } -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 = /^\[|^\{(?!\{)/; @@ -23545,12 +22431,6 @@ 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)) { @@ -23584,7 +22464,7 @@ function $HttpParamSerializerProvider() { forEachSorted(params, function(value, key) { if (value === null || isUndefined(value)) return; if (isArray(value)) { - forEach(value, function(v) { + forEach(value, function(v, k) { parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); }); } else { @@ -23651,8 +22531,8 @@ function $HttpParamSerializerJQLikeProvider() { 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 : '') + ']'); + forEach(toSerialize, function(value) { + serialize(value, prefix + '[]'); }); } else if (isObject(toSerialize) && !isDate(toSerialize)) { forEachSorted(toSerialize, function(value, key) { @@ -23794,9 +22674,10 @@ function $HttpProvider() { * * 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.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} + * that will provide the cache for all requests who set their `cache` property to `true`. + * If you set the `defaults.cache = false` then only requests that specify their own custom + * cache object will be cached. See {@link $http#caching $http Caching} for more information. * * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. * Defaults value is `'XSRF-TOKEN'`. @@ -23872,30 +22753,6 @@ function $HttpProvider() { 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 @@ -23961,47 +22818,66 @@ function $HttpProvider() { * * * ## 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}. + * The `$http` service is a function which takes a single argument — a configuration object — + * that is used to generate an HTTP request and returns a {@link ng.$q promise} + * with two $http specific methods: `success` and `error`. * * ```js - * // Simple GET request example: - * $http({ - * method: 'GET', - * url: '/someUrl' - * }).then(function successCallback(response) { + * // Simple GET request example : + * $http.get('/someUrl'). + * success(function(data, status, headers, config) { * // this callback will be called asynchronously * // when the response is available - * }, function errorCallback(response) { + * }). + * error(function(data, status, headers, config) { * // called asynchronously if an error occurs * // or server returns response with an error status. * }); * ``` * - * The response object has these properties: + * ```js + * // Simple POST request example (passing data) : + * $http.post('/someUrl', {msg:'hello word!'}). + * success(function(data, status, headers, config) { + * // this callback will be called asynchronously + * // when the response is available + * }). + * error(function(data, status, headers, config) { + * // called asynchronously if an error occurs + * // or server returns response with an error status. + * }); + * ``` * - * - **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. + * + * Since the returned value of calling the $http function is a `promise`, you can also use + * the `then` method to register callbacks, and these callbacks will receive a single argument – + * an object representing the response. See the API signature and type info below for more + * details. * * A response status code between 200 and 299 is considered a success status and * will result in the success callback being called. Note that if the response is a redirect, * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * + * ## 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(); + * ``` * * ## 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. + * request data must be passed in for POST/PUT requests. * * ```js - * $http.get('/someUrl', config).then(successCallback, errorCallback); - * $http.post('/someUrl', data, config).then(successCallback, errorCallback); + * $http.get('/someUrl').success(successCallback); + * $http.post('/someUrl', data).success(successCallback); * ``` * * Complete list of shortcut methods: @@ -24015,25 +22891,6 @@ function $HttpProvider() { * - {@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 @@ -24057,7 +22914,7 @@ function $HttpProvider() { * * ``` * module.run(function($http) { - * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'; + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' * }); * ``` * @@ -24077,7 +22934,7 @@ function $HttpProvider() { * data: { test: 'test' } * } * - * $http(req).then(function(){...}, function(){...}); + * $http(req).success(function(){...}).error(function(){...}); * ``` * * ## Transforming Requests and Responses @@ -24087,15 +22944,6 @@ function $HttpProvider() { * 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 his, 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 @@ -24153,35 +23001,26 @@ function $HttpProvider() { * * ## 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 + * To enable caching, set the request configuration `cache` property to `true` (to use default + * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). + * When the cache is enabled, `$http` stores the response from the server in the specified + * cache. The next time the same request is made, the response is served from the cache without + * sending a request to the server. * - * 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. + * Note that even if the response is served from cache, delivery of the data is asynchronous in + * the same way that real requests are. * - * 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. + * If there are multiple GET requests for the same URL that should be cached using the same + * cache, but the cache is not populated yet, only one request to the server will be made and + * the remaining requests will be fulfilled using the response from the first request. * - * 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. + * You can change the default cache to a new object (built with + * {@link ng.$cacheFactory `$cacheFactory`}) by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set + * their `cache` property to `true` will now use this cache object. * + * If you set the default cache to `false` then only requests that specify their own custom + * cache object will be cached. * * ## Interceptors * @@ -24201,7 +23040,7 @@ function $HttpProvider() { * * 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 + * * `request`: interceptors get called with a http `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 @@ -24303,13 +23142,13 @@ function $HttpProvider() { * * ### 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. + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which + * an unauthorized site can gain your user's private data. 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 @@ -24327,6 +23166,7 @@ function $HttpProvider() { * 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: * @@ -24351,7 +23191,7 @@ function $HttpProvider() { * 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} + * Overriding the Default TransformationjqLiks} * - **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 @@ -24359,9 +23199,10 @@ function $HttpProvider() { * 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. + * - **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|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 @@ -24370,9 +23211,20 @@ function $HttpProvider() { * - **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. + * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the + * standard `then` method and two http specific methods: `success` and `error`. The `then` + * method takes two arguments a success and an error callback which will be called with a + * response object. The `success` and `error` methods take a single argument - a function that + * will be called when the request succeeds or fails respectively. The arguments passed into + * these functions are destructured representation of the response object passed into the + * `then` method. 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. * * @property {Array.<Object>} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. @@ -24414,12 +23266,13 @@ function $HttpProvider() { $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; + success(function(data, status) { + $scope.status = status; + $scope.data = data; + }). + error(function(data, status) { + $scope.data = data || "Request failed"; + $scope.status = status; }); }; @@ -24467,14 +23320,10 @@ function $HttpProvider() { */ function $http(requestConfig) { - if (!isObject(requestConfig)) { + if (!angular.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, @@ -24528,36 +23377,34 @@ function $HttpProvider() { promise = promise.then(thenFn, rejectFn); } - if (useLegacyPromise) { - promise.success = function(fn) { - assertArgFn(fn, 'fn'); + promise.success = function(fn) { + assertArgFn(fn, 'fn'); - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; - promise.error = function(fn) { - assertArgFn(fn, 'fn'); + 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'); - } + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; return promise; 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); + if (!response.data) { + resp.data = response.data; + } else { + resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); + } return (isSuccess(response.status)) ? resp : $q.reject(resp); @@ -24587,7 +23434,7 @@ function $HttpProvider() { defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // using for-in instead of forEach to avoid unnecessary iteration after header has been found + // using for-in instead of forEach to avoid unecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { lowercaseDefHeaderName = lowercase(defHeaderName); @@ -24835,8 +23682,8 @@ function $HttpProvider() { * 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; + // normalize internal statuses to 0 + status = Math.max(status, 0); (isSuccess(status) ? deferred.resolve : deferred.reject)({ data: response, @@ -24867,33 +23714,8 @@ function $HttpProvider() { }]; } -/** - * @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(); - }; - }; +function createXhr() { + return new window.XMLHttpRequest(); } /** @@ -24901,7 +23723,6 @@ function $xhrFactoryProvider() { * @name $httpBackend * @requires $window * @requires $document - * @requires $xhrFactory * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -24914,8 +23735,8 @@ function $xhrFactoryProvider() { * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) { - return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]); + this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { + return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); }]; } @@ -24939,7 +23760,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc }); } else { - var xhr = createXhr(method, url); + var xhr = createXhr(); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -24951,7 +23772,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc xhr.onload = function requestLoaded() { var statusText = xhr.statusText || ''; - // responseText is the old-school way of retrieving response (supported by IE9) + // responseText is the old-school way of retrieving response (supported by IE8 & 9) // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) var response = ('response' in xhr) ? xhr.response : xhr.responseText; @@ -25002,7 +23823,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } } - xhr.send(isUndefined(post) ? null : post); + xhr.send(post); } if (timeout > 0) { @@ -25019,7 +23840,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution - if (isDefined(timeoutId)) { + if (timeoutId !== undefined) { $browserDefer.cancel(timeoutId); } jsonpDone = xhr = null; @@ -25086,16 +23907,8 @@ $interpolateMinErr.interr = function(text, err) { * * 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"> +<example module="customInterpolationApp"> <file name="index.html"> <script> var customInterpolationApp = angular.module('customInterpolationApp', []); @@ -25110,7 +23923,7 @@ $interpolateMinErr.interr = function(text, err) { this.label = "This binding is brought you by // interpolation symbols."; }); </script> -<div ng-controller="DemoController as demo"> +<div ng-app="App" ng-controller="DemoController as demo"> //demo.label// </div> </file> @@ -25194,15 +24007,6 @@ function $InterpolateProvider() { 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 @@ -25222,7 +24026,7 @@ function $InterpolateProvider() { * ```js * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name | uppercase}}!'); - * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!'); + * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); * ``` * * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is @@ -25298,19 +24102,6 @@ function $InterpolateProvider() { * - `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, @@ -25447,8 +24238,8 @@ function $InterpolateProvider() { } function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', - function($rootScope, $window, $q, $$q, $browser) { + this.$get = ['$rootScope', '$window', '$q', '$$q', + function($rootScope, $window, $q, $$q) { var intervals = {}; @@ -25589,12 +24380,11 @@ function $IntervalProvider() { count = isDefined(count) ? count : 0; + promise.then(null, null, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); + promise.$$intervalId = setInterval(function tick() { - if (skipApply) { - $browser.defer(callback); - } else { - $rootScope.$evalAsync(callback); - } deferred.notify(iteration++); if (count > 0 && iteration >= count) { @@ -25610,14 +24400,6 @@ function $IntervalProvider() { intervals[promise.$$intervalId] = deferred; return promise; - - function callback() { - if (!hasParams) { - fn(iteration); - } else { - fn.apply(null, args); - } - } } @@ -25628,7 +24410,7 @@ function $IntervalProvider() { * @description * Cancels a task associated with the `promise`. * - * @param {Promise=} promise returned by the `$interval` function. + * @param {promise} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { @@ -25655,6 +24437,75 @@ function $IntervalProvider() { * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ +function $LocaleProvider() { + this.$get = function() { + return { + id: 'en-us', + + NUMBER_FORMATS: { + DECIMAL_SEP: '.', + GROUP_SEP: ',', + PATTERNS: [ + { // Decimal Pattern + minInt: 1, + minFrac: 0, + maxFrac: 3, + posPre: '', + posSuf: '', + negPre: '-', + negSuf: '', + gSize: 3, + lgSize: 3 + },{ //Currency Pattern + minInt: 1, + minFrac: 2, + maxFrac: 2, + posPre: '\u00A4', + posSuf: '', + negPre: '(\u00A4', + negSuf: ')', + gSize: 3, + lgSize: 3 + } + ], + CURRENCY_SYM: '$' + }, + + DATETIME_FORMATS: { + MONTH: + 'January,February,March,April,May,June,July,August,September,October,November,December' + .split(','), + SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), + DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), + SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), + AMPMS: ['AM','PM'], + medium: 'MMM d, y h:mm:ss a', + 'short': 'M/d/yy h:mm a', + fullDate: 'EEEE, MMMM d, y', + longDate: 'MMMM d, y', + mediumDate: 'MMM d, y', + shortDate: 'M/d/yy', + mediumTime: 'h:mm:ss a', + shortTime: 'h:mm a', + ERANAMES: [ + "Before Christ", + "Anno Domini" + ], + ERAS: [ + "BC", + "AD" + ] + }, + + pluralCat: function(num) { + if (num === 1) { + return 'one'; + } + return 'other'; + } + }; + }; +} var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; @@ -25745,12 +24596,12 @@ function serverBase(url) { * * @constructor * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} basePrefix url path prefix */ -function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { +function LocationHtml5Url(appBase, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; + var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this); @@ -25797,14 +24648,14 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; - if (isDefined(appUrl = beginsWith(appBase, url))) { + if ((appUrl = beginsWith(appBase, url)) !== undefined) { prevAppUrl = appUrl; - if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) { + if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } - } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) { + } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { rewrittenUrl = appBaseNoFile; @@ -25824,10 +24675,10 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { * * @constructor * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this); @@ -25936,13 +24787,14 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { * * @constructor * @param {string} appBase application base URL - * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { +function LocationHashbangInHtml5Url(appBase, hashPrefix) { this.$$html5 = true; LocationHashbangUrl.apply(this, arguments); + var appBaseNoFile = stripFile(appBase); + this.$$parseLinkUrl = function(url, relHref) { if (relHref && relHref[0] === '#') { // special case for links to hash fragments: @@ -25972,7 +24824,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' this.$$absUrl = appBase + hashPrefix + this.$$url; }; @@ -26230,9 +25082,9 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Returns the hash fragment when called without any parameters. + * Return hash fragment when called without any parameter. * - * Changes the hash fragment when called with a parameter and returns `$location`. + * Change hash fragment when called with parameter and return `$location`. * * * ```js @@ -26253,8 +25105,8 @@ var locationPrototype = { * @name $location#replace * * @description - * If called, all changes to $location during the current `$digest` will replace the current history - * record, instead of adding a new one. + * If called, all changes to $location during current `$digest` will be replacing current history + * record, instead of adding new one. */ replace: function() { this.$$replace = true; @@ -26481,9 +25333,7 @@ function $LocationProvider() { appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } - var appBaseNoFile = stripFile(appBase); - - $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix); + $location = new LocationMode(appBase, '#' + hashPrefix); $location.$$parseLinkUrl(initialUrl, initialUrl); $location.$$state = $browser.state(); @@ -26563,18 +25413,11 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - - if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { - // If we are navigating outside of the app then force a reload - $window.location.href = newUrl; - return; - } - $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; - newUrl = trimEmptyHash(newUrl); + $location.$$parse(newUrl); $location.$$state = newState; @@ -26857,24 +25700,6 @@ function ensureSafeMemberName(name, 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 - // into a string via the toString method. - // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names - // - // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it - // to a string. It's not always possible. If `name` is an object and its `toString` method is - // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: - // - // TypeError: Cannot convert object to primitive value - // - // For performance reasons, we don't catch this error here and allow it to propagate up the call - // stack. Note that you'll get the same error in JavaScript if you try to access a property using - // such a 'broken' object as a key. - return name + ''; -} - function ensureSafeObject(obj, fullExpression) { // nifty check if obj is Function that is fast and works across iframes and other contexts if (obj) { @@ -26920,16 +25745,6 @@ function ensureSafeFunction(obj, 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", "'":"'", '"':'"'}; @@ -27133,7 +25948,6 @@ AST.ArrayExpression = 'ArrayExpression'; AST.Property = 'Property'; AST.ObjectExpression = 'ObjectExpression'; AST.ThisExpression = 'ThisExpression'; -AST.LocalsExpression = 'LocalsExpression'; // Internal use only AST.NGValueParameter = 'NGValueParameter'; @@ -27272,10 +26086,8 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { - primary = copy(this.selfReferential[this.consume().text]); - } else if (this.options.literals.hasOwnProperty(this.peek().text)) { - primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; + } else if (this.constants.hasOwnProperty(this.peek().text)) { + primary = copy(this.constants[this.consume().text]); } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -27427,9 +26239,16 @@ AST.prototype = { return false; }, - selfReferential: { - 'this': {type: AST.ThisExpression }, - '$locals': {type: AST.LocalsExpression } + + /* `undefined` is not a constant, it is an identifier, + * but using it as an identifier is not supported + */ + constants: { + 'true': { type: AST.Literal, value: true }, + 'false': { type: AST.Literal, value: false }, + 'null': { type: AST.Literal, value: null }, + 'undefined': {type: AST.Literal, value: undefined }, + 'this': {type: AST.ThisExpression } } }; @@ -27549,10 +26368,6 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.constant = false; ast.toWatch = []; break; - case AST.LocalsExpression: - ast.constant = false; - ast.toWatch = []; - break; } } @@ -27611,7 +26426,6 @@ ASTCompiler.prototype = { this.state.computing = 'assign'; var result = this.nextId(); this.recurse(assignable, result); - this.return_(result); extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); } var toWatch = getInputs(ast.body); @@ -27644,8 +26458,6 @@ ASTCompiler.prototype = { 'ensureSafeMemberName', 'ensureSafeObject', 'ensureSafeFunction', - 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', 'text', @@ -27654,8 +26466,6 @@ ASTCompiler.prototype = { ensureSafeMemberName, ensureSafeObject, ensureSafeFunction, - getStringValue, - ensureSafeAssignContext, ifDefined, plusFn, expression); @@ -27796,13 +26606,9 @@ 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), '{}')); @@ -27880,13 +26686,12 @@ ASTCompiler.prototype = { right = this.nextId(); left = {}; if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + throw $parseMinErr('lval', 'Trying to assing 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); @@ -27922,10 +26727,6 @@ ASTCompiler.prototype = { this.assign(intoId, 's'); recursionFn('s'); break; - case AST.LocalsExpression: - this.assign(intoId, 'l'); - recursionFn('l'); - break; case AST.NGValueParameter: this.assign(intoId, 'v'); recursionFn('v'); @@ -28016,10 +26817,6 @@ ASTCompiler.prototype = { this.current().body.push(this.ensureSafeFunction(item), ';'); }, - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - ensureSafeObject: function(item) { return 'ensureSafeObject(' + item + ',text)'; }, @@ -28032,14 +26829,6 @@ ASTCompiler.prototype = { 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() { @@ -28117,7 +26906,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? noop : + var fn = ast.body.length === 0 ? function() {} : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -28217,7 +27006,6 @@ ASTInterpreter.prototype = { 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; }; @@ -28253,12 +27041,8 @@ ASTInterpreter.prototype = { return function(scope) { return context ? {value: scope} : scope; }; - case AST.LocalsExpression: - return function(scope, locals) { - return context ? {value: locals} : locals; - }; case AST.NGValueParameter: - return function(scope, locals, assign) { + return function(scope, locals, assign, inputs) { return context ? {value: assign} : assign; }; } @@ -28419,13 +27203,9 @@ ASTInterpreter.prototype = { var value; 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] = {}; - } + if (create && create !== 1 && lhs && !(lhs[rhs])) { + lhs[rhs] = {}; } value = lhs[rhs]; ensureSafeObject(value, expression); @@ -28440,11 +27220,8 @@ ASTInterpreter.prototype = { nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); - if (create && create !== 1) { - ensureSafeAssignContext(lhs); - if (lhs && !(lhs[right])) { - lhs[right] = {}; - } + if (create && create !== 1 && lhs && !(lhs[right])) { + lhs[right] = {}; } var value = lhs != null ? lhs[right] : undefined; if (expensiveChecks || isPossiblyDangerousMemberName(right)) { @@ -28472,7 +27249,7 @@ var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; - this.ast = new AST(lexer, options); + this.ast = new AST(this.lexer); this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : new ASTCompiler(this.ast, $filter); }; @@ -28485,6 +27262,32 @@ Parser.prototype = { } }; +////////////////////////////////////////////////// +// Parser helper functions +////////////////////////////////////////////////// + +function setter(obj, path, setValue, fullExp) { + ensureSafeObject(obj, fullExp); + + var element = path.split('.'), key; + for (var i = 0; element.length > 1; i++) { + key = ensureSafeMemberName(element.shift(), fullExp); + var propertyObj = ensureSafeObject(obj[key], fullExp); + if (!propertyObj) { + propertyObj = {}; + obj[key] = propertyObj; + } + obj = propertyObj; + } + key = ensureSafeMemberName(element.shift(), fullExp); + ensureSafeObject(obj[key], fullExp); + obj[key] = setValue; + return setValue; +} + +var getterFnCacheDefault = createMap(); +var getterFnCacheExpensive = createMap(); + function isPossiblyDangerousMemberName(name) { return name == 'constructor'; } @@ -28549,53 +27352,20 @@ function getValueOf(value) { function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); - var literals = { - 'true': true, - 'false': false, - 'null': null, - 'undefined': undefined - }; - - /** - * @ngdoc method - * @name $parseProvider#addLiteral - * @description - * - * Configure $parse service to add literal values that will be present as literal at expressions. - * - * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. - * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. - * - **/ - this.addLiteral = function(literalName, literalValue) { - literals[literalName] = literalValue; - }; - this.$get = ['$filter', function($filter) { - var noUnsafeEval = csp().noUnsafeEval; + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { var $parseOptions = { - csp: noUnsafeEval, - expensiveChecks: false, - literals: copy(literals) + csp: $sniffer.csp, + expensiveChecks: false }, $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true, - literals: copy(literals) + csp: $sniffer.csp, + expensiveChecks: true }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - - return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { + return function $parse(exp, interceptorFn, expensiveChecks) { var parsedExpression, oneTime, cacheKey; - expensiveChecks = expensiveChecks || runningChecksEnabled; - switch (typeof exp) { case 'string': exp = exp.trim(); @@ -28621,9 +27391,6 @@ function $ParseProvider() { } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -28632,33 +27399,9 @@ function $ParseProvider() { return addInterceptor(exp, interceptorFn); default: - return addInterceptor(noop, interceptorFn); - } - } - - 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]); + return noop; } - 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 expressionInputDirtyCheck(newValue, oldValueOfValue) { @@ -28775,22 +27518,25 @@ function $ParseProvider() { function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch; return unwatch = scope.$watch(function constantWatch(scope) { - unwatch(); return parsedExpression(scope); - }, listener, objectEquality); + }, function constantListener(value, old, scope) { + if (isFunction(listener)) { + listener.apply(this, arguments); + } + unwatch(); + }, objectEquality); } function addInterceptor(parsedExpression, interceptorFn) { if (!interceptorFn) return parsedExpression; var watchDelegate = parsedExpression.$$watchDelegate; - var useInputs = false; var regularWatch = watchDelegate !== oneTimeLiteralWatchDelegate && watchDelegate !== oneTimeWatchDelegate; var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { - var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); + var value = parsedExpression(scope, locals, assign, inputs); return interceptorFn(value, scope, locals); } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { var value = parsedExpression(scope, locals, assign, inputs); @@ -28808,7 +27554,6 @@ function $ParseProvider() { // 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; - useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } @@ -28830,15 +27575,15 @@ function $ParseProvider() { * [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. + * implementations, and the other which resembles ES6 promises to some degree. * * # $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` - * function as the first argument. This is similar to the native Promise implementation from ES6, + * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * - * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are + * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are * available yet. * * It can be used like so: @@ -28870,8 +27615,6 @@ function $ParseProvider() { * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * - * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. - * * However, the more traditional CommonJS-style usage is still available, and documented below. * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an @@ -29003,7 +27746,7 @@ function $ParseProvider() { * - 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) { @@ -29060,6 +27803,18 @@ function $$QProvider() { */ function qFactory(nextTick, exceptionHandler) { var $qMinErr = minErr('$q', TypeError); + function callOnce(self, resolveFn, rejectFn) { + var called = false; + function wrap(fn) { + return function(value) { + if (called) return; + called = true; + fn.call(self, value); + }; + } + + return [wrap(resolveFn), wrap(rejectFn)]; + } /** * @ngdoc method @@ -29072,23 +27827,15 @@ 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; + return new Deferred(); }; function Promise() { this.$$state = { status: 0 }; } - extend(Promise.prototype, { + Promise.prototype = { then: function(onFulfilled, onRejected, progressBack) { - if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { - return this; - } var result = new Deferred(); this.$$state.pending = this.$$state.pending || []; @@ -29109,7 +27856,7 @@ function qFactory(nextTick, exceptionHandler) { return handleCallback(error, false, callback); }, progressBack); } - }); + }; //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native function simpleBind(context, fn) { @@ -29150,9 +27897,13 @@ function qFactory(nextTick, exceptionHandler) { function Deferred() { this.promise = new Promise(); + //Necessary to support unbound execution :/ + this.resolve = simpleBind(this, this.resolve); + this.reject = simpleBind(this, this.reject); + this.notify = simpleBind(this, this.notify); } - extend(Deferred.prototype, { + Deferred.prototype = { resolve: function(val) { if (this.promise.$$state.status) return; if (val === this.promise) { @@ -29167,34 +27918,23 @@ function qFactory(nextTick, exceptionHandler) { }, $$resolve: function(val) { - var then; - var that = this; - var done = false; + var then, fns; + + fns = callOnce(this, this.$$resolve, this.$$reject); 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)); + then.call(val, fns[0], fns[1], this.notify); } else { this.promise.$$state.value = val; this.promise.$$state.status = 1; scheduleProcessQueue(this.promise.$$state); } } catch (e) { - rejectPromise(e); + fns[1](e); exceptionHandler(e); } - - function resolvePromise(val) { - if (done) return; - done = true; - that.$$resolve(val); - } - function rejectPromise(val) { - if (done) return; - done = true; - that.$$reject(val); - } }, reject: function(reason) { @@ -29226,7 +27966,7 @@ function qFactory(nextTick, exceptionHandler) { }); } } - }); + }; /** * @ngdoc method @@ -29309,9 +28049,6 @@ function qFactory(nextTick, exceptionHandler) { * the promise comes from a source that can't be trusted. * * @param {*} value Value or a promise - * @param {Function=} successCallback - * @param {Function=} errorCallback - * @param {Function=} progressCallback * @returns {Promise} Returns a promise of the passed value or promise */ @@ -29331,9 +28068,6 @@ function qFactory(nextTick, exceptionHandler) { * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6. * * @param {*} value Value or a promise - * @param {Function=} successCallback - * @param {Function=} errorCallback - * @param {Function=} progressCallback * @returns {Promise} Returns a promise of the passed value or promise */ var resolve = when; @@ -29383,6 +28117,11 @@ function qFactory(nextTick, exceptionHandler) { throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); } + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + var deferred = new Deferred(); function resolveFn(value) { @@ -29398,10 +28137,6 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; }; - // Let's make the instanceof operator work for promises, so that - // `new $q(fn) instanceof $q` would evaluate to true. - $Q.prototype = Promise.prototype; - $Q.defer = defer; $Q.reject = reject; $Q.when = when; @@ -29421,7 +28156,7 @@ function $$RAFProvider() { //rAF $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; - var raf = rafSupported + var rafFn = rafSupported ? function(fn) { var id = requestAnimationFrame(fn); return function() { @@ -29435,9 +28170,47 @@ function $$RAFProvider() { //rAF }; }; - raf.supported = rafSupported; + queueFn.supported = rafSupported; + + var cancelLastRAF; + var taskCount = 0; + var taskQueue = []; + return queueFn; + + function flush() { + for (var i = 0; i < taskQueue.length; i++) { + var task = taskQueue[i]; + if (task) { + taskQueue[i] = null; + task(); + } + } + taskCount = taskQueue.length = 0; + } + + function queueFn(asyncFn) { + var index = taskQueue.length; + + taskCount++; + taskQueue.push(asyncFn); - return raf; + if (index === 0) { + cancelLastRAF = rafFn(flush); + } + + return function cancelQueueFn() { + if (index >= 0) { + taskQueue[index] = null; + index = null; + + if (--taskCount === 0 && cancelLastRAF) { + cancelLastRAF(); + cancelLastRAF = null; + taskQueue.length = 0; + } + } + }; + } }]; } @@ -29455,15 +28228,15 @@ function $$RAFProvider() { //rAF * exposed as $$____ properties * * Loop operations are optimized by using while(count--) { ... } - * - This means that in order to keep the same order of execution as addition we have to add + * - this means that in order to keep the same order of execution as addition we have to add * items to the array at the beginning (unshift) instead of at the end (push) * * Child scopes are created and removed often - * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists + * - Using an array would be slow since inserts in middle are expensive so we use linked list * - * There are fewer watches than observers. This is why you don't want the observer to be implemented - * in the same way as watch. Watch requires return of the initialization function which is expensive - * to construct. + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. */ @@ -29505,7 +28278,7 @@ function $$RAFProvider() { //rAF * Every application has a single root {@link ng.$rootScope.Scope scope}. * All other scopes are descendant scopes of the root scope. Scopes provide separation * between the model and the view, via a mechanism for watching the model for changes. - * They also provide event emission/broadcast and subscription facility. See the + * They also provide an event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider() { @@ -29535,36 +28308,13 @@ function $RootScopeProvider() { return ChildScope; } - this.$get = ['$exceptionHandler', '$parse', '$browser', - function($exceptionHandler, $parse, $browser) { + this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', + function($injector, $exceptionHandler, $parse, $browser) { function destroyChildScope($event) { $event.currentScope.$$destroyed = true; } - function cleanUpScope($scope) { - - 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); - } - - // The code below works around IE9 and V8's memory leaks - // - // See: - // - https://code.google.com/p/v8/issues/detail?id=2073#c26 - // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 - // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - - $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead = - $scope.$$childTail = $scope.$root = $scope.$$watchers = null; - } - /** * @ngdoc type * @name $rootScope.Scope @@ -29573,9 +28323,12 @@ function $RootScopeProvider() { * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the * {@link auto.$injector $injector}. Child scopes are created using the * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) See also the {@link guide/scope Scopes guide} for - * an in-depth introduction and usage examples. + * compiled HTML template is executed.) * + * Here is a simple scope snippet to show how you can interact with the scope. + * ```html + * <file src="./test/ng/rootScopeSpec.js" tag="docs1" /> + * ``` * * # Inheritance * A scope can inherit from a parent scope, as in this example: @@ -29717,10 +28470,10 @@ function $RootScopeProvider() { * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest - * $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). + * $digest()} and should return the value that will be watched. (Since + * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the + * `watchExpression` can execute multiple times per + * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - 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, @@ -29737,9 +28490,9 @@ function $RootScopeProvider() { * * * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Be prepared for - * multiple calls to your `watchExpression` because it will execute multiple times in a - * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.) + * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` + * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a + * change is detected, be prepared for multiple calls to your listener.) * * After a watcher is registered with the scope, the `listener` fn is called asynchronously * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the @@ -29820,7 +28573,7 @@ function $RootScopeProvider() { * - `newVal` contains the current value of the `watchExpression` * - `oldVal` contains the previous value of the `watchExpression` * - `scope` refers to the current scope - * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of + * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ @@ -30069,7 +28822,7 @@ function $RootScopeProvider() { // copy the items to oldValue and look for changes. newLength = 0; for (key in newValue) { - if (hasOwnProperty.call(newValue, key)) { + if (newValue.hasOwnProperty(key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; @@ -30091,7 +28844,7 @@ function $RootScopeProvider() { // we used to have more keys, need to find them and destroy them. changeDetected++; for (key in oldValue) { - if (!hasOwnProperty.call(newValue, key)) { + if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } @@ -30185,13 +28938,13 @@ function $RootScopeProvider() { * */ $digest: function() { - var watch, value, last, fn, get, + var watch, value, last, watchers, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, asyncTask; + logIdx, logMsg, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -30231,8 +28984,7 @@ function $RootScopeProvider() { // 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) { - get = watch.get; - if ((value = get(current)) !== (last = watch.last) && + if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' @@ -30240,8 +28992,7 @@ function $RootScopeProvider() { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; - fn = watch.fn; - fn(value, ((last === initWatchVal) ? value : last), current); + watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; @@ -30363,9 +29114,16 @@ function $RootScopeProvider() { this.$on = this.$watch = this.$watchGroup = function() { return noop; }; this.$$listeners = {}; - // Disconnect the next sibling to prevent `cleanUpScope` destroying those too - this.$$nextSibling = null; - cleanUpScope(this); + // All of the code below is bogus code that works around V8's memory leak via optimized code + // and inline caches. + // + // see: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = + this.$$childTail = this.$root = this.$$watchers = null; }, /** @@ -30441,7 +29199,7 @@ function $RootScopeProvider() { }); } - asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); + asyncQueue.push({scope: this, expression: expr, locals: locals}); }, $$postDigest: function(fn) { @@ -30496,14 +29254,11 @@ function $RootScopeProvider() { $apply: function(expr) { try { beginPhase('$apply'); - try { - return this.$eval(expr); - } finally { - clearPhase(); - } + return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { + clearPhase(); try { $rootScope.$digest(); } catch (e) { @@ -30533,7 +29288,6 @@ function $RootScopeProvider() { $applyAsync: function(expr) { var scope = this; expr && applyAsyncQueue.push($applyAsyncExpression); - expr = $parse(expr); scheduleApplyAsync(); function $applyAsyncExpression() { @@ -30809,21 +29563,6 @@ function $RootScopeProvider() { } /** - * @ngdoc service - * @name $rootElement - * - * @description - * The root element of Angular 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 - * published, and can be retrieved using `$rootElement.injector()`. - */ - - -// the implementation is in angular.bootstrap - -/** * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ @@ -31037,15 +29776,13 @@ function $SceDelegateProvider() { * @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. + * 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. * - * <div class="alert alert-warning"> - * **Note:** an empty whitelist array will block all URLs! - * </div> + * Note: **an empty whitelist array will block all URLs**! * * @return {Array} the currently set whitelist array. * @@ -31068,17 +29805,17 @@ function $SceDelegateProvider() { * @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. + * 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. * - * 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. + * 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. + * Finally, **the blacklist overrides the whitelist** and has the final say. * * @return {Array} the currently set blacklist array. * @@ -31189,7 +29926,7 @@ function $SceDelegateProvider() { 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', type, trustedValue); } - if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') { + if (trustedValue === null || trustedValue === undefined || trustedValue === '') { return trustedValue; } // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting @@ -31237,11 +29974,6 @@ function $SceDelegateProvider() { * 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. * - * <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 {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} call. @@ -31249,7 +29981,7 @@ function $SceDelegateProvider() { * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. */ function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { + if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -31384,7 +30116,7 @@ function $SceDelegateProvider() { * By default, Angular 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 + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. * * *Please note*: @@ -31442,10 +30174,10 @@ function $SceDelegateProvider() { * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters * 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 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use * in a whitelist. * - `**`: 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. + * not appropriate to use in for 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 * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). @@ -31453,11 +30185,11 @@ function $SceDelegateProvider() { * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to * accidentally introduce a bug when one updates a complex expression (imho, all regexes should - * have good test coverage). For instance, the use of `.` in the regex is correct only in a + * have good test coverage.). For instance, the use of `.` in the regex is correct only in a * small number of cases. A `.` character in the regex used when matching the scheme or a * subdomain could be matched against a `:` or literal `.` that was likely not intended. It * is highly recommended to use the string patterns and only fall back to regular expressions - * as a last resort. + * if they as a last resort. * - The regular expression must be an instance of RegExp (i.e. not a string.) It is * matched against the **entire** *normalized / absolute URL* of the resource being tested * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags @@ -31467,7 +30199,7 @@ function $SceDelegateProvider() { * remember to escape your regular expression (and be aware that you might need more than * one level of escaping depending on your templating engine and the way you interpolated * the value.) Do make use of your platform's escaping mechanism as it might be good - * enough before coding your own. E.g. Ruby has + * enough before coding your own. e.g. Ruby has * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). * Javascript lacks a similar built in function for escaping. Take a look at Google @@ -31976,10 +30708,6 @@ 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, - hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -32024,7 +30752,7 @@ function $SnifferProvider() { // 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), + history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have @@ -32050,66 +30778,29 @@ function $SnifferProvider() { }]; } -var $templateRequestMinErr = minErr('$compile'); +var $compileMinErr = minErr('$compile'); /** - * @ngdoc provider - * @name $templateRequestProvider + * @ngdoc service + * @name $templateRequest + * * @description - * Used to configure the options passed to the {@link $http} service when making a template request. + * The `$templateRequest` service runs security checks then downloads the provided template using + * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request + * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the + * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the + * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted + * when `tpl` is of type string and `$templateCache` has the matching entry. + * + * @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 * - * For example, it can be used for specifying the "Accept" header that is sent to the server, when - * requesting a template. + * @return {Promise} a promise for the HTTP response data of the given URL. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. */ function $TemplateRequestProvider() { - - var httpOptions; - - /** - * @ngdoc method - * @name $templateRequestProvider#httpOptions - * @description - * The options to be passed to the {@link $http} service when making the request. - * You can use this to override options such as the "Accept" header for template requests. - * - * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the - * options if not overridden here. - * - * @param {string=} value new value for the {@link $http} options. - * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter. - */ - this.httpOptions = function(val) { - if (val) { - httpOptions = val; - return this; - } - return httpOptions; - }; - - /** - * @ngdoc service - * @name $templateRequest - * - * @description - * The `$templateRequest` service runs security checks then downloads the provided template using - * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request - * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the - * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the - * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted - * when `tpl` is of type string and `$templateCache` has the matching entry. - * - * 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}. - * - * @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 - * - * @return {Promise} a promise for the HTTP response data of the given URL. - * - * @property {number} totalPendingRequests total amount of pending template requests being downloaded. - */ this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { - function handleRequestFn(tpl, ignoreRequestError) { handleRequestFn.totalPendingRequests++; @@ -32132,10 +30823,12 @@ function $TemplateRequestProvider() { transformResponse = null; } - return $http.get(tpl, extend({ - cache: $templateCache, - transformResponse: transformResponse - }, httpOptions)) + var httpOptions = { + cache: $templateCache, + transformResponse: transformResponse + }; + + return $http.get(tpl, httpOptions) ['finally'](function() { handleRequestFn.totalPendingRequests--; }) @@ -32146,7 +30839,7 @@ function $TemplateRequestProvider() { function handleError(resp) { if (!ignoreRequestError) { - throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', + throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', tpl, resp.status, resp.statusText); } return $q.reject(resp); @@ -32306,8 +30999,8 @@ function $TimeoutProvider() { * @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. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this + * promise will be resolved with is the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { @@ -32394,13 +31087,20 @@ var originUrl = urlResolve(window.location.href); * * Implementation Notes for IE * --------------------------- - * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other * browsers. However, the parsed components will not be set if the URL assigned did not specify * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We * work around that by performing the parsing in a 2nd step by taking a previously normalized * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the * properties such as protocol, hostname, port, etc. * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * * References: * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html @@ -32549,7 +31249,7 @@ function $$CookieReader($document) { // the first value that is seen for a cookie is the most // specific one. values for the same cookie name that // follow are for less specific paths. - if (isUndefined(lastCookies[name])) { + if (lastCookies[name] === undefined) { lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); } } @@ -32680,7 +31380,6 @@ function $FilterProvider($provide) { * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores * (`myapp_subsection_filterx`). * </div> - * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ @@ -32983,10 +31682,6 @@ function getTypeForFilter(val) { return (val === null) ? 'null' : typeof val; } -var MAX_DIGITS = 22; -var DECIMAL_SEP = '.'; -var ZERO_CHAR = '0'; - /** * @ngdoc filter * @name currency @@ -33032,9 +31727,9 @@ var ZERO_CHAR = '0'; } element(by.model('amount')).clear(); element(by.model('amount')).sendKeys('-1234'); - expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00'); - expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00'); - expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234'); + expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); + expect(element(by.id('currency-custom')).getText()).toBe('(USD$1,234.00)'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('(USD$1,234)'); }); </file> </example> @@ -33068,7 +31763,7 @@ function currencyFilter($locale) { * Formats a number as text. * * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. + * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. * If the input is not a number an empty string is returned. * * @@ -33076,7 +31771,7 @@ function currencyFilter($locale) { * @param {(number|string)=} fractionSize Number of decimal places to round the number to. * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to fractionSize and places a “,” after each third digit. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example <example module="numberFilterExample"> @@ -33111,6 +31806,8 @@ function currencyFilter($locale) { </file> </example> */ + + numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; @@ -33124,227 +31821,102 @@ function numberFilter($locale) { }; } -/** - * Parse a number (as a string) into three components that can be used - * for formatting the number. - * - * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) - * - * @param {string} numStr The number to parse - * @return {object} An object describing this number, containing the following keys: - * - d : an array of digits containing leading zeros as necessary - * - i : the number of the digits in `d` that are to the left of the decimal point - * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` - * - */ -function parse(numStr) { - var exponent = 0, digits, numberOfIntegerDigits; - var i, j, zeros; - - // Decimal point? - if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { - numStr = numStr.replace(DECIMAL_SEP, ''); - } - - // Exponential form? - if ((i = numStr.search(/e/i)) > 0) { - // Work out the exponent. - if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; - numberOfIntegerDigits += +numStr.slice(i + 1); - numStr = numStr.substring(0, i); - } else if (numberOfIntegerDigits < 0) { - // There was no decimal point or exponent so it is an integer. - numberOfIntegerDigits = numStr.length; - } - - // Count the number of leading zeros. - for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */} - - 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--; - - // Trailing zeros are insignificant so ignore them - numberOfIntegerDigits -= i; - digits = []; - // Convert string to array of digits without leading/trailing zeros. - for (j = 0; i <= zeros; i++, j++) { - digits[j] = +numStr.charAt(i); - } - } - - // If the number overflows the maximum allowed digits then use an exponent. - if (numberOfIntegerDigits > MAX_DIGITS) { - digits = digits.splice(0, MAX_DIGITS - 1); - exponent = numberOfIntegerDigits - 1; - numberOfIntegerDigits = 1; - } - - return { d: digits, e: exponent, i: numberOfIntegerDigits }; -} +var DECIMAL_SEP = '.'; +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + if (isObject(number)) return ''; -/** - * Round the parsed number to the specified number of decimal places - * This function changed the parsedNumber in-place - */ -function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { - var digits = parsedNumber.d; - var fractionLen = digits.length - parsedNumber.i; + var isNegative = number < 0; + number = Math.abs(number); - // determine fractionSize if it is not specified; `+fractionSize` converts it to a number - fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; + var isInfinity = number === Infinity; + if (!isInfinity && !isFinite(number)) return ''; - // The index of the digit to where rounding is to occur - var roundAt = fractionSize + parsedNumber.i; - var digit = digits[roundAt]; + var numStr = number + '', + formatedText = '', + hasExponent = false, + parts = []; - if (roundAt > 0) { - // Drop fractional digits beyond `roundAt` - digits.splice(Math.max(parsedNumber.i, roundAt)); + if (isInfinity) formatedText = '\u221e'; - // Set non-fractional digits beyond `roundAt` to 0 - for (var j = roundAt; j < digits.length; j++) { - digits[j] = 0; - } + if (!isInfinity && numStr.indexOf('e') !== -1) { + var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); + if (match && match[2] == '-' && match[3] > fractionSize + 1) { + number = 0; } else { - // We rounded to zero so reset the parsedNumber - fractionLen = Math.max(0, fractionLen); - parsedNumber.i = 1; - digits.length = Math.max(1, roundAt = fractionSize + 1); - digits[0] = 0; - for (var i = 1; i < roundAt; i++) digits[i] = 0; - } - - if (digit >= 5) { - if (roundAt - 1 < 0) { - for (var k = 0; k > roundAt; k--) { - digits.unshift(0); - parsedNumber.i++; - } - digits.unshift(1); - parsedNumber.i++; - } else { - digits[roundAt - 1]++; - } + formatedText = numStr; + hasExponent = true; } + } - // Pad out with zeros to get the required fraction length - for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); - + if (!isInfinity && !hasExponent) { + var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; - // Do any carrying, e.g. a digit was rounded up to 10 - var carry = digits.reduceRight(function(carry, d, i, digits) { - d = d + carry; - digits[i] = d % 10; - return Math.floor(d / 10); - }, 0); - if (carry) { - digits.unshift(carry); - parsedNumber.i++; + // determine fractionSize if it is not specified + if (isUndefined(fractionSize)) { + fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } -} - -/** - * Format a number into a string - * @param {number} number The number to format - * @param {{ - * minFrac, // the minimum number of digits required in the fraction part of the number - * maxFrac, // the maximum number of digits required in the fraction part of the number - * gSize, // number of digits in each group of separated digits - * lgSize, // number of digits in the last group of digits before the decimal separator - * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) - * posPre, // the string to go in front of a positive number - * negSuf, // the string to go after a negative number (e.g. `)`) - * posSuf // the string to go after a positive number - * }} pattern - * @param {string} groupSep The string to separate groups of number (e.g. `,`) - * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) - * @param {[type]} fractionSize The size of the fractional part of the number - * @return {string} The number formatted as a string - */ -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (!(isString(number) || isNumber(number)) || isNaN(number)) return ''; + // safely round numbers in JS without hitting imprecisions of floating-point arithmetics + // inspired by: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); - var isInfinity = !isFinite(number); - var isZero = false; - var numStr = Math.abs(number) + '', - formattedText = '', - parsedNumber; + var fraction = ('' + number).split(DECIMAL_SEP); + var whole = fraction[0]; + fraction = fraction[1] || ''; - if (isInfinity) { - formattedText = '\u221e'; - } else { - parsedNumber = parse(numStr); - - roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); - - var digits = parsedNumber.d; - var integerLen = parsedNumber.i; - var exponent = parsedNumber.e; - var decimals = []; - isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true); - - // pad zeros for small numbers - while (integerLen < 0) { - digits.unshift(0); - integerLen++; - } + var i, pos = 0, + lgroup = pattern.lgSize, + group = pattern.gSize; - // extract decimals digits - if (integerLen > 0) { - decimals = digits.splice(integerLen); - } else { - decimals = digits; - digits = [0]; + if (whole.length >= (lgroup + group)) { + pos = whole.length - lgroup; + for (i = 0; i < pos; i++) { + if ((pos - i) % group === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } } - // format the integer digits with grouping separators - var groups = []; - if (digits.length >= pattern.lgSize) { - groups.unshift(digits.splice(-pattern.lgSize).join('')); - } - while (digits.length > pattern.gSize) { - groups.unshift(digits.splice(-pattern.gSize).join('')); - } - if (digits.length) { - groups.unshift(digits.join('')); + for (i = pos; i < whole.length; i++) { + if ((whole.length - i) % lgroup === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); } - formattedText = groups.join(groupSep); - // append the decimal digits - if (decimals.length) { - formattedText += decimalSep + decimals.join(''); + // format fraction part. + while (fraction.length < fractionSize) { + fraction += '0'; } - if (exponent) { - formattedText += 'e+' + exponent; + if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); + } else { + if (fractionSize > 0 && number < 1) { + formatedText = number.toFixed(fractionSize); + number = parseFloat(formatedText); } } - if (number < 0 && !isZero) { - return pattern.negPre + formattedText + pattern.negSuf; - } else { - return pattern.posPre + formattedText + pattern.posSuf; + + if (number === 0) { + isNegative = false; } + + parts.push(isNegative ? pattern.negPre : pattern.posPre, + formatedText, + isNegative ? pattern.negSuf : pattern.posSuf); + return parts.join(''); } -function padNumber(num, digits, trim, negWrap) { +function padNumber(num, digits, trim) { var neg = ''; - if (num < 0 || (negWrap && num <= 0)) { - if (negWrap) { - num = -num + 1; - } else { - num = -num; - neg = '-'; - } + if (num < 0) { + neg = '-'; + num = -num; } num = '' + num; - while (num.length < digits) num = ZERO_CHAR + num; + while (num.length < digits) num = '0' + num; if (trim) { num = num.substr(num.length - digits); } @@ -33352,7 +31924,7 @@ function padNumber(num, digits, trim, negWrap) { } -function dateGetter(name, size, offset, trim, negWrap) { +function dateGetter(name, size, offset, trim) { offset = offset || 0; return function(date) { var value = date['get' + name](); @@ -33360,15 +31932,14 @@ function dateGetter(name, size, offset, trim, negWrap) { value += offset; } if (value === 0 && offset == -12) value = 12; - return padNumber(value, size, trim, negWrap); + return padNumber(value, size, trim); }; } -function dateStrGetter(name, shortForm, standAlone) { +function dateStrGetter(name, shortForm) { return function(date, formats) { var value = date['get' + name](); - var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); - var get = uppercase(propPrefix + name); + var get = uppercase(shortForm ? ('SHORT' + name) : name); return formats[get][value]; }; @@ -33423,14 +31994,13 @@ function longEraGetter(date, formats) { } var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4, 0, false, true), - yy: dateGetter('FullYear', 2, 0, true, true), - y: dateGetter('FullYear', 1, 0, false, true), + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, true), + y: dateGetter('FullYear', 1), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), - LLLL: dateStrGetter('Month', false, true), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), @@ -33456,7 +32026,7 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** @@ -33476,7 +32046,6 @@ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+ * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) - * * `'LLLL'`: Stand-alone month in year (January-December) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) @@ -33616,13 +32185,13 @@ function dateFilter($locale) { var dateTimezoneOffset = date.getTimezoneOffset(); if (timezone) { - dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); date = convertTimezoneToLocal(date, timezone, true); } forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) - : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); }); return text; @@ -33803,7 +32372,7 @@ function limitToFilter() { if (!isArray(input) && !isString(input)) return input; begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); - begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; + begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin; if (limit >= 0) { return input.slice(begin, begin + limit); @@ -33826,9 +32395,8 @@ function limitToFilter() { * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically * for strings and numerically for numbers. Note: if you notice numbers are not being sorted * as expected, make sure they are actually being saved as numbers and not strings. - * Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported. * - * @param {Array} array The array (or array-like object) to sort. + * @param {Array} array The array to sort. * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * @@ -33859,6 +32427,17 @@ function limitToFilter() { * `reverse` is not set, which means it defaults to `false`. <example module="orderByExample"> <file name="index.html"> + <script> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}]; + }]); + </script> <div ng-controller="ExampleController"> <table class="friend"> <tr> @@ -33874,17 +32453,6 @@ function limitToFilter() { </table> </div> </file> - <file name="script.js"> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}]; - }]); - </file> </example> * * The predicate and reverse parameters can be controlled dynamically through scope properties, @@ -33892,24 +32460,49 @@ function limitToFilter() { * @example <example module="orderByExample"> <file name="index.html"> + <script> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}]; + $scope.predicate = 'age'; + $scope.reverse = true; + $scope.order = function(predicate) { + $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; + $scope.predicate = predicate; + }; + }]); + </script> + <style type="text/css"> + .sortorder:after { + content: '\25b2'; + } + .sortorder.reverse:after { + content: '\25bc'; + } + </style> <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> - <button ng-click="predicate=''">Set to unsorted</button> + [ <a href="" ng-click="predicate=''">unsorted</a> ] <table class="friend"> <tr> - <th> - <button ng-click="order('name')">Name</button> - <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> - </th> - <th> - <button ng-click="order('phone')">Phone Number</button> - <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> - </th> - <th> - <button ng-click="order('age')">Age</button> - <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> - </th> + <th> + <a href="" ng-click="order('name')">Name</a> + <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <a href="" ng-click="order('phone')">Phone Number</a> + <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <a href="" ng-click="order('age')">Age</a> + <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> + </th> </tr> <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> <td>{{friend.name}}</td> @@ -33919,31 +32512,6 @@ function limitToFilter() { </table> </div> </file> - <file name="script.js"> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}]; - $scope.predicate = 'age'; - $scope.reverse = true; - $scope.order = function(predicate) { - $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; - $scope.predicate = predicate; - }; - }]); - </file> - <file name="style.css"> - .sortorder:after { - content: '\25b2'; - } - .sortorder.reverse:after { - content: '\25bc'; - } - </file> </example> * * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the @@ -33955,30 +32523,21 @@ function limitToFilter() { * @example <example module="orderByExample"> <file name="index.html"> - <div ng-controller="ExampleController"> - <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> - <table class="friend"> - <tr> - <th> - <button ng-click="order('name')">Name</button> - <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> - </th> - <th> - <button ng-click="order('phone')">Phone Number</button> - <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> - </th> - <th> - <button ng-click="order('age')">Age</button> - <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> - </th> - </tr> - <tr ng-repeat="friend in friends"> - <td>{{friend.name}}</td> - <td>{{friend.phone}}</td> - <td>{{friend.age}}</td> - </tr> - </table> - </div> + <div ng-controller="ExampleController"> + <table class="friend"> + <tr> + <th><a href="" ng-click="reverse=false;order('name', false)">Name</a> + (<a href="" ng-click="order('-name',false)">^</a>)</th> + <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th> + <th><a href="" ng-click="reverse=!reverse;order('age',reverse)">Age</a></th> + </tr> + <tr ng-repeat="friend in friends"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <td>{{friend.age}}</td> + </tr> + </table> + </div> </file> <file name="script.js"> @@ -33992,42 +32551,24 @@ function limitToFilter() { { name: 'Adam', phone: '555-5678', age: 35 }, { name: 'Julie', phone: '555-8765', age: 29 } ]; - $scope.order = function(predicate) { - $scope.predicate = predicate; - $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; - $scope.friends = orderBy($scope.friends, predicate, $scope.reverse); + $scope.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); }; - $scope.order('age', true); + $scope.order('-age',false); }]); </file> - - <file name="style.css"> - .sortorder:after { - content: '\25b2'; - } - .sortorder.reverse:after { - content: '\25bc'; - } - </file> </example> */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse) { return function(array, sortPredicate, reverseOrder) { - if (array == null) return array; - if (!isArrayLike(array)) { - throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array); - } + if (!(isArrayLike(array))) return array; if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } if (sortPredicate.length === 0) { sortPredicate = ['+']; } var predicates = processPredicates(sortPredicate, reverseOrder); - // Add a predicate at the end that evaluates to the element index. This makes the - // sort stable as it works as a tie-breaker when all the input predicates cannot - // distinguish between two elements. - predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1}); // The next three lines are a version of a Swartzian Transform idiom from Perl // (sometimes called the Decorate-Sort-Undecorate idiom) @@ -34341,7 +32882,20 @@ var htmlAnchorDirective = valueFn({ * {@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. + * attribute. The following example would make the button enabled on Chrome/Firefox + * but not on older IEs: + * + * ```html + * <!-- See below for an example of ng-disabled being used correctly --> + * <div ng-init="isDisabled = false"> + * <button disabled="{{isDisabled}}">Disabled</button> + * </div> + * ``` + * + * This is because the HTML specification does not require browsers to preserve the values of + * boolean attributes such as `disabled` (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. * * @example <example> @@ -34376,9 +32930,15 @@ var htmlAnchorDirective = valueFn({ * Note that this directive should not be used together with {@link ngModel `ngModel`}, * as this can lead to unexpected behavior. * - * A special directive is necessary because we cannot use interpolation inside the `checked` - * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * ### Why do we need `ngChecked`? * + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as checked. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngChecked` directive solves this problem for the `checked` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * @example <example> <file name="index.html"> @@ -34407,12 +32967,13 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * - * Sets the `readOnly` attribute on the element, if the expression inside `ngReadonly` is truthy. - * - * A special directive is necessary because we cannot use interpolation inside the `readOnly` - * attribute. See the {@link guide/interpolation interpolation guide} for more info. - * + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as readonly. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngReadonly` directive solves this problem for the `readonly` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * @example <example> <file name="index.html"> @@ -34441,11 +33002,13 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * - * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy. - * - * A special directive is necessary because we cannot use interpolation inside the `selected` - * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as selected. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngSelected` directive solves this problem for the `selected` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * * @example <example> @@ -34477,12 +33040,13 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * - * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy. - * - * A special directive is necessary because we cannot use interpolation inside the `open` - * attribute. See the {@link guide/interpolation interpolation guide} for more info. - * + * The HTML specification does not require browsers to preserve the values of boolean attributes + * such as open. (Their presence means true and their absence means false.) + * If we put an Angular interpolation expression into such an attribute then the + * binding information would be lost when the browser removes the attribute. + * The `ngOpen` directive solves this problem for the `open` attribute. + * This complementary directive is not removed by the browser and so provides + * a permanent reliable place to store the binding information. * @example <example> <file name="index.html"> @@ -34626,7 +33190,6 @@ 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 @@ -34666,6 +33229,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, controls = []; + var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + // init state form.$error = {}; form.$$success = {}; @@ -34676,7 +33241,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { form.$valid = true; form.$invalid = false; form.$submitted = false; - form.$$parentForm = nullFormCtrl; + + parentForm.$addControl(form); /** * @ngdoc method @@ -34715,23 +33281,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { /** * @ngdoc method * @name form.FormController#$addControl - * @param {object} control control object, either a {@link form.FormController} or an - * {@link ngModel.NgModelController} * * @description - * Register a control with the form. Input elements using ngModelController do this automatically - * when they are linked. - * - * Note that the current state of the control will not be reflected on the new parent form. This - * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine` - * state. + * Register a control with the form. * - * However, if the method is used programmatically, for example by adding dynamically created controls, - * or controls that have been previously removed without destroying their corresponding DOM element, - * it's the developers responsibility to make sure the current state propagates to the parent form. - * - * 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. + * Input elements using ngModelController do this automatically when they are linked. */ form.$addControl = function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored @@ -34742,8 +33296,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { if (control.$name) { form[control.$name] = control; } - - control.$$parentForm = form; }; // Private API: rename a form control @@ -34760,18 +33312,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { /** * @ngdoc method * @name form.FormController#$removeControl - * @param {object} control control object, either a {@link form.FormController} or an - * {@link ngModel.NgModelController} * * @description * Deregister a control from the form. * * Input elements using ngModelController do this automatically when they are destroyed. - * - * Note that only the removed control's validation state (`$errors`etc.) will be removed from the - * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be - * 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) { @@ -34788,7 +33333,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { }); arrayRemove(controls, control); - control.$$parentForm = nullFormCtrl; }; @@ -34825,6 +33369,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { delete object[property]; } }, + parentForm: parentForm, $animate: $animate }); @@ -34843,7 +33388,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { $animate.addClass(element, DIRTY_CLASS); form.$dirty = true; form.$pristine = false; - form.$$parentForm.$setDirty(); + parentForm.$setDirty(); }; /** @@ -34899,7 +33444,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { form.$setSubmitted = function() { $animate.addClass(element, SUBMITTED_CLASS); form.$submitted = true; - form.$$parentForm.$setSubmitted(); + parentForm.$setSubmitted(); }; } @@ -34938,14 +33483,17 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * In Angular, 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 - * `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. + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to + * `<form>` but can be nested. This allows you to have nested forms, which is very useful when + * using Angular validation directives in forms that are dynamically generated using the + * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` + * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an + * `ngForm` directive and nest these in an outer `form` element. + * * * # 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. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. * - `ng-submitted` is set if the form was submitted. @@ -35021,6 +33569,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { </script> <style> .my-form { + -webkit-transition:all linear 0.5s; transition:all linear 0.5s; background: transparent; } @@ -35065,11 +33614,10 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { - return ['$timeout', '$parse', function($timeout, $parse) { + return ['$timeout', function($timeout) { var formDirective = { name: 'form', restrict: isNgForm ? 'EAC' : 'E', - require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form controller: FormController, compile: function ngFormCompile(formElement, attr) { // Setup initial state of the control @@ -35078,9 +33626,7 @@ var formDirectiveFactory = function(isNgForm) { var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); return { - pre: function ngFormPreLink(scope, formElement, attr, ctrls) { - var controller = ctrls[0]; - + pre: function ngFormPreLink(scope, formElement, attr, controller) { // if `action` attr is not present on the form, prevent the default action (submission) if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default @@ -35109,24 +33655,22 @@ var formDirectiveFactory = function(isNgForm) { }); } - var parentFormCtrl = ctrls[1] || controller.$$parentForm; - parentFormCtrl.$addControl(controller); - - var setter = nameAttr ? getSetter(controller.$name) : noop; + var parentFormCtrl = controller.$$parentForm; if (nameAttr) { - setter(scope, controller); + setter(scope, controller.$name, controller, controller.$name); attr.$observe(nameAttr, function(newValue) { if (controller.$name === newValue) return; - setter(scope, undefined); - controller.$$parentForm.$$renameControl(controller, newValue); - setter = getSetter(controller.$name); - setter(scope, controller); + setter(scope, controller.$name, undefined, controller.$name); + parentFormCtrl.$$renameControl(controller, newValue); + setter(scope, controller.$name, controller, controller.$name); }); } formElement.on('$destroy', function() { - controller.$$parentForm.$removeControl(controller); - setter(scope, undefined); + parentFormCtrl.$removeControl(controller); + if (nameAttr) { + setter(scope, attr[nameAttr], undefined, controller.$name); + } extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); } @@ -35135,14 +33679,6 @@ var formDirectiveFactory = function(isNgForm) { }; return formDirective; - - function getSetter(expression) { - if (expression === '') { - //create an assignable expression, so forms with an empty name can be renamed later - return $parse('this[""]').assign; - } - return $parse(expression).assign || noop; - } }]; }; @@ -35155,38 +33691,20 @@ var ngFormDirective = formDirectiveFactory(true); 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 -var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; -// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) -// Note: We are being more lenient, because browsers are too. -// 1. Scheme -// 2. Slashes -// 3. Username -// 4. Password -// 5. Hostname -// 6. Port -// 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; +// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; +var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; 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)$/; -var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/; +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)$/; +var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; -var PARTIAL_VALIDATION_TYPES = createMap(); -forEach('date,datetime-local,month,time,week'.split(','), function(type) { - PARTIAL_VALIDATION_TYPES[type] = true; -}); - var inputType = { /** @@ -35211,8 +33729,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -35305,17 +33823,9 @@ var inputType = { * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute - * (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5 - * constraint validation. + * valid ISO date string (yyyy-MM-dd). * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute - * (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5 - * constraint validation. - * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string - * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. - * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string - * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * a valid ISO date string (yyyy-MM-dd). * @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 @@ -35407,18 +33917,10 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation - * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). - * Note that `min` will also add native HTML5 constraint validation. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation - * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). - * Note that `max` will also add native HTML5 constraint validation. - * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string - * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. - * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string - * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @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). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). * @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 @@ -35499,7 +34001,7 @@ var inputType = { * * @description * Input with time validation and transformation. In browsers that do not yet support - * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 * 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)`. * @@ -35511,18 +34013,10 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this - * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add - * native HTML5 constraint validation. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this - * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add - * native HTML5 constraint validation. - * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the - * `ngMin` expression evaluates to. Note that it does not set the `min` attribute. - * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the - * `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @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). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a + * valid ISO time format (HH:mm:ss). * @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 @@ -35542,7 +34036,7 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - <label for="exampleInput">Pick a time between 8am and 5pm:</label> + <label for="exampleInput">Pick a between 8am and 5pm:</label> <input type="time" id="exampleInput" name="input" ng-model="example.value" placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required /> <div role="alert"> @@ -35614,18 +34108,10 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this - * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add - * native HTML5 constraint validation. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this - * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add - * native HTML5 constraint validation. - * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string - * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. - * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string - * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @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##). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO week format (yyyy-W##). * @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 @@ -35719,19 +34205,10 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. - * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this - * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add - * native HTML5 constraint validation. - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. - * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this - * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add - * native HTML5 constraint validation. - * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string - * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. - * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string - * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. - + * @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). + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must + * be a valid ISO month format (yyyy-MM). * @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 @@ -35846,8 +34323,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -35944,8 +34421,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -36043,8 +34520,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -36263,7 +34740,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function() { + element.on('compositionstart', function(data) { composing = true; }); @@ -36273,8 +34750,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - var timeout; - var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -36304,6 +34779,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { + var timeout; + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { @@ -36335,32 +34812,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); - // Some native input types (date-family) have the ability to change validity without - // firing any input/change events. - // 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) { - if (!timeout) { - var validity = this[VALIDITY_STATE_PROPERTY]; - var origBadInput = validity.badInput; - var origTypeMismatch = validity.typeMismatch; - timeout = $browser.defer(function() { - timeout = null; - if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { - listener(ev); - } - }); - } - }); - } - ctrl.$render = function() { - // Workaround for Firefox validation #12102. - var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; - if (element.val() !== value) { - element.val(value); - } + element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; } @@ -36471,7 +34924,7 @@ function createDateInputType(type, regexp, parseDate, format) { ctrl.$formatters.push(function(value) { if (value && !isDate(value)) { - throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); } if (isValidDate(value)) { previousDate = value; @@ -36513,7 +34966,7 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseObservedDateValue(val) { - return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; } }; } @@ -36524,7 +34977,11 @@ function badInputChecker(scope, element, attr, ctrl) { if (nativeValidation) { ctrl.$parsers.push(function(value) { var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; - return validity.badInput || validity.typeMismatch ? undefined : value; + // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): + // - also sets validity.badInput (should only be validity.typeMismatch). + // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) + // - can ignore this case as we can still read out the erroneous email... + return validity.badInput && !validity.typeMismatch ? undefined : value; }); } } @@ -36543,7 +35000,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$formatters.push(function(value) { if (!ctrl.$isEmpty(value)) { if (!isNumber(value)) { - throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); } value = value.toString(); } @@ -36636,7 +35093,7 @@ function parseConstantExpr($parse, context, name, expression, fallback) { if (isDefined(expression)) { parseFn = $parse(expression); if (!parseFn.constant) { - throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' + + throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + '`{1}`.', name, expression); } return parseFn(context); @@ -36696,8 +35153,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -36735,8 +35192,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * 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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular 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 @@ -37004,7 +35461,7 @@ var ngBindDirective = ['$compile', function($compile) { $compile.$$addBindingInfo(element, attr.ngBind); element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - element.textContent = isUndefined(value) ? '' : value; + element.textContent = value === undefined ? '' : value; }); }; } @@ -37072,7 +35529,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate $compile.$$addBindingInfo(element, interpolateFn.expressions); element = element[0]; attr.$observe('ngBindTemplate', function(value) { - element.textContent = isUndefined(value) ? '' : value; + element.textContent = value === undefined ? '' : value; }); }; } @@ -37375,10 +35832,9 @@ function classDirective(name, selector) { * new classes added. * * @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 | + * **add** - happens just before the class is applied to the elements + * + * **remove** - happens just before the class is removed from the element * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result @@ -37490,6 +35946,7 @@ function classDirective(name, selector) { </file> <file name="style.css"> .base-class { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -37922,29 +36379,27 @@ var ngControllerDirective = [function() { * * @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. + * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. * * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps. * + * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). + * For Angular to be CSP compatible there are only two things that we need to do differently: * - * The following rules affect Angular: + * - don't use `Function` constructor to generate optimized value getters + * - don't inject custom stylesheet into the document * - * * `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. + * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` + * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will + * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will + * be raised. * - * * `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. + * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically + * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). + * To make those directives work in CSP mode, include the `angular-csp.css` manually. * - * 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: + * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This + * autodetection however triggers a CSP error to be logged in the console: * * ``` * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of @@ -37953,39 +36408,11 @@ var ngControllerDirective = [function() { * ``` * * 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. + * directive on the root element of the application or on the `angular.js` script tag, whichever + * appears first in the html document. * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * - * 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 @@ -38117,7 +36544,7 @@ var ngControllerDirective = [function() { // 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 +// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc /** * @ngdoc directive @@ -38637,10 +37064,8 @@ forEach( * 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 | + * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container + * leave - happens just before the `ngIf` contents are removed from the DOM * * @element ANY * @scope @@ -38666,6 +37091,7 @@ forEach( } .animate-if.ng-enter, .animate-if.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -38681,7 +37107,7 @@ forEach( </file> </example> */ -var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { +var ngIfDirective = ['$animate', function($animate) { return { multiElement: true, transclude: 'element', @@ -38697,7 +37123,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { if (!childScope) { $transclude(function(clone, newScope) { childScope = newScope; - clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); + clone[clone.length++] = document.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. @@ -38752,10 +37178,8 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { * 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 | + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. * * The enter and leave animation occur concurrently. * @@ -38765,13 +37189,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { * @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. * @@ -38822,6 +37240,7 @@ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { } .slide-animate.ng-enter, .slide-animate.ng-leave { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; position:absolute; @@ -38963,8 +37382,6 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', //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; @@ -38986,8 +37403,6 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', currentScope.$emit('$includeContentLoaded', src); scope.$eval(onloadExp); }, function() { - if (scope.$$destroyed) return; - if (thisChangeId === changeCounter) { cleanupLastIncludeContent(); scope.$emit('$includeContentError', src); @@ -39016,7 +37431,7 @@ var ngIncludeFillContentDirective = ['$compile', priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { - if (toString.call($element[0]).match(/SVG/)) { + if (/SVG/.test($element[0].toString())) { // 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. @@ -39044,18 +37459,16 @@ var ngIncludeFillContentDirective = ['$compile', * 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. + * The only appropriate use of `ngInit` is for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, 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: + * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make + * sure you have parenthesis for correct precedence: * <pre class="prettyprint"> - * `<div ng-init="test1 = ($index | toString)"></div>` + * `<div ng-init="test1 = (data | orderBy:'name')"></div>` * </pre> * </div> * @@ -39168,7 +37581,7 @@ var ngInitDirective = ngDirective({ * </file> * </example> * - * ### Example - splitting on newline + * ### Example - splitting on whitespace * <example name="ngList-directive-newlines"> * <file name="index.html"> * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> @@ -39245,19 +37658,16 @@ var VALID_CLASS = 'ng-valid', 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'; + PENDING_CLASS = 'ng-pending'; + -var ngModelMinErr = minErr('ngModel'); +var $ngModelMinErr = new 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 {string} $viewValue Actual string value in the view. * @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 @@ -39471,7 +37881,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ 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, @@ -39496,13 +37906,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; ngModelSet = function($scope, newValue) { if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: newValue}); + invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); } else { - parsedNgModelAssign($scope, newValue); + parsedNgModelAssign($scope, ctrl.$modelValue); } }; } else if (!parsedNgModel.assign) { - throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", $attr.ngModel, startingTag($element)); } }; @@ -39523,7 +37933,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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` + * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ @@ -39551,18 +37961,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ 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; + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, + currentValidationRunId = 0; /** * @ngdoc method @@ -39595,6 +37995,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ unset: function(object, property) { delete object[property]; }, + parentForm: parentForm, $animate: $animate }); @@ -39632,7 +38033,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$pristine = false; $animate.removeClass($element, PRISTINE_CLASS); $animate.addClass($element, DIRTY_CLASS); - ctrl.$$parentForm.$setDirty(); + parentForm.$setDirty(); }; /** @@ -39679,14 +38080,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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. + * If you have an input that uses `ng-model-options` to set up debounced events or events such + * as blur you can have a situation where there is a period when the `$viewValue` + * is out of synch with the ngModel's `$modelValue`. * - * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` + * In this case, you can 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. * @@ -39699,63 +38097,39 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * angular.module('cancel-update-example', []) * * .controller('CancelUpdateController', ['$scope', function($scope) { - * $scope.model = {}; - * - * $scope.setEmpty = function(e, value, rollback) { + * $scope.resetWithCancel = function(e) { + * if (e.keyCode == 27) { + * $scope.myForm.myInput1.$rollbackViewValue(); + * $scope.myValue = ''; + * } + * }; + * $scope.resetWithoutCancel = function(e) { * if (e.keyCode == 27) { - * e.preventDefault(); - * if (rollback) { - * $scope.myForm[value].$rollbackViewValue(); - * } - * $scope.model[value] = ''; + * $scope.myValue = ''; * } * }; * }]); * </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> + * <p>Try typing something in each input. See that the model only updates when you + * blur off the input. + * </p> + * <p>Now see what happens if you start typing then press the Escape key</p> * * <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> + * <p id="inputDescription1">With $rollbackViewValue()</p> + * <input name="myInput1" aria-describedby="inputDescription1" ng-model="myValue" + * ng-keydown="resetWithCancel($event)"><br/> + * myValue: "{{ myValue }}" + * + * <p id="inputDescription2">Without $rollbackViewValue()</p> + * <input name="myInput2" aria-describedby="inputDescription2" ng-model="myValue" + * ng-keydown="resetWithoutCancel($event)"><br/> + * myValue: "{{ myValue }}" * </form> * </div> * </file> - <file name="style.css"> - div { - display: table-cell; - } - div:nth-child(1) { - padding-right: 30px; - } - - </file> * </example> */ this.$rollbackViewValue = function() { @@ -39829,7 +38203,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processParseErrors() { var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + if (parserValid === undefined) { setValidity(errorKey, null); } else { if (!parserValid) { @@ -39869,13 +38243,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ forEach(ctrl.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { - throw ngModelMinErr('nopromise', + throw $ngModelMinErr("$asyncValidators", "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); } setValidity(name, undefined); validatorPromises.push(promise.then(function() { setValidity(name, true); - }, function() { + }, function(error) { allValid = false; setValidity(name, false); })); @@ -39925,7 +38299,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { return; } - ctrl.$$updateEmptyClasses(viewValue); ctrl.$$lastCommittedViewValue = viewValue; // change to dirty @@ -40000,47 +38373,37 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @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. + * This method should be called when an input directive want to change the view value; typically, + * this is done from within a DOM event handler. + * + * For example {@link ng.directive:input input} calls it when the value of the input changes and + * {@link ng.directive:select select} calls it when an option is selected. + * + * If the new `value` is an object (rather than a string or a number), we should make a copy of the + * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep + * watch of objects, it only looks for a change of identity. If you only change the property of + * the object then ngModel will not realise that the object has changed and will not invoke the + * `$parsers` and `$validators` pipelines. + * + * For this reason, you should not change properties of the copy once it has been passed to + * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. * - * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` + * When this method is called, the new `value` will be staged for committing through the `$parsers` * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged * value sent directly for processing, finally to be applied to `$modelValue` and then the - * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners, - * in the `$viewChangeListeners` list, are called. + * **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. + * Note that calling this function does not trigger a `$digest`. + * + * @param {string} value Value from the view. * @param {string} trigger Event that triggered the update. */ this.$setViewValue = function(value, trigger) { @@ -40108,7 +38471,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ viewValue = formatters[idx](viewValue); } if (ctrl.$viewValue !== viewValue) { - ctrl.$$updateEmptyClasses(viewValue); ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); @@ -40139,8 +38501,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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. + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) 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 @@ -40168,22 +38529,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@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. @@ -40197,16 +38542,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - `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`, + * on the input element which is attached to the model. These classes are: `.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. @@ -40238,6 +38580,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </script> <style> .my-input { + -webkit-transition:all linear 0.5s; transition:all linear 0.5s; background: transparent; } @@ -40324,7 +38667,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { return { pre: function ngModelPreLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || modelCtrl.$$parentForm; + formCtrl = ctrls[1] || nullFormCtrl; modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); @@ -40333,12 +38676,12 @@ var ngModelDirective = ['$rootScope', function($rootScope) { attr.$observe('name', function(newValue) { if (modelCtrl.$name !== newValue) { - modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue); + formCtrl.$$renameControl(modelCtrl, newValue); } }); scope.$on('$destroy', function() { - modelCtrl.$$parentForm.$removeControl(modelCtrl); + formCtrl.$removeControl(modelCtrl); }); }, post: function ngModelPostLink(scope, element, attr, ctrls) { @@ -40349,7 +38692,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }); } - element.on('blur', function() { + element.on('blur', function(ev) { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { @@ -40430,13 +38773,12 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; </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.user = { name: 'say', data: '' }; $scope.cancel = function(e) { if (e.keyCode == 27) { @@ -40451,20 +38793,20 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; var other = element(by.model('user.data')); it('should allow custom events', function() { - input.sendKeys(' Doe'); + input.sendKeys(' hello'); input.click(); - expect(model.getText()).toEqual('John'); + expect(model.getText()).toEqual('say'); other.click(); - expect(model.getText()).toEqual('John Doe'); + expect(model.getText()).toEqual('say hello'); }); it('should $rollbackViewValue when model changes', function() { - input.sendKeys(' Doe'); - expect(input.getAttribute('value')).toEqual('John Doe'); + input.sendKeys(' hello'); + expect(input.getAttribute('value')).toEqual('say hello'); input.sendKeys(protractor.Key.ESCAPE); - expect(input.getAttribute('value')).toEqual('John'); + expect(input.getAttribute('value')).toEqual('say'); other.click(); - expect(model.getText()).toEqual('John'); + expect(model.getText()).toEqual('say'); }); </file> </example> @@ -40490,7 +38832,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; <file name="app.js"> angular.module('optionsExample', []) .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'Igor' }; + $scope.user = { name: 'say' }; }]); </file> </example> @@ -40534,7 +38876,7 @@ var ngModelOptionsDirective = function() { var that = this; this.$options = copy($scope.$eval($attrs.ngModelOptions)); // Allow adding/overriding bound events - if (isDefined(this.$options.updateOn)) { + if (this.$options.updateOn !== undefined) { 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() { @@ -40557,6 +38899,7 @@ function addSetValidityMethod(context) { classCache = {}, set = context.set, unset = context.unset, + parentForm = context.parentForm, $animate = context.$animate; classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); @@ -40564,7 +38907,7 @@ function addSetValidityMethod(context) { ctrl.$setValidity = setValidity; function setValidity(validationErrorKey, state, controller) { - if (isUndefined(state)) { + if (state === undefined) { createAndSet('$pending', validationErrorKey, controller); } else { unsetAndCleanup('$pending', validationErrorKey, controller); @@ -40608,7 +38951,7 @@ function addSetValidityMethod(context) { } toggleValidationCss(validationErrorKey, combinedState); - ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + parentForm.$setValidity(validationErrorKey, combinedState, ctrl); } function createAndSet(name, value, controller) { @@ -40723,27 +39066,19 @@ var ngOptionsMinErr = minErr('ngOptions'); * * ## Complex Models (objects or collections) * - * By default, `ngModel` watches the model by reference, not value. This is important to know when - * binding the select to a model that is an object or a collection. + * **Note:** By default, `ngModel` watches the model by reference, not value. This is important when + * binding any input directive to a model that is an object or a collection. * - * One issue occurs if you want to preselect an option. For example, if you set - * the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection, - * because the objects are not identical. So by default, you should always reference the item in your collection - * for preselections, e.g.: `$scope.selected = $scope.collection[3]`. - * - * Another solution is to use a `track by` clause, because then `ngOptions` will track the identity - * of the item not by reference, but by the result of the `track by` expression. For example, if your - * collection items have an id property, you would `track by item.id`. - * - * A different issue with objects or collections is that ngModel won't detect if an object property or - * a collection item changes. For that reason, `ngOptions` additionally watches the model using - * `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute. - * This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection - * has not changed identity, but only a property on the object or an item in the collection changes. + * Since this is a common situation for `ngOptions` the directive additionally watches the model using + * `$watchCollection` when the select has the `multiple` attribute or when there is a `track by` clause in + * the options expression. This allows ngOptions to trigger a re-rendering of the options even if the actual + * object/collection has not changed identity but only a property on the object or an item in the collection + * changes. * * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection - * if the model is an array). This means that changing a property deeper than the first level inside the - * object/collection will not trigger a re-rendering. + * if the model is an array). This means that changing a property deeper inside the object/collection that the + * first level will not trigger a re-rendering. + * * * ## `select` **`as`** * @@ -40756,13 +39091,17 @@ var ngOptionsMinErr = minErr('ngOptions'); * ### `select` **`as`** and **`track by`** * * <div class="alert alert-warning"> - * Be careful when using `select` **`as`** and **`track by`** in the same expression. + * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together. * </div> * - * Given this array of items on the $scope: + * Consider the following example: + * + * ```html + * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> + * ``` * * ```js - * $scope.items = [{ + * $scope.values = [{ * id: 1, * label: 'aLabel', * subItem: { name: 'aSubItem' } @@ -40771,33 +39110,20 @@ var ngOptionsMinErr = minErr('ngOptions'); * label: 'bLabel', * subItem: { name: 'bSubItem' } * }]; - * ``` - * - * This will work: * - * ```html - * <select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select> - * ``` - * ```js - * $scope.selected = $scope.items[0]; + * $scope.selected = { name: 'aSubItem' }; * ``` * - * but this will not work: + * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element + * of the data source (to `item` in this example). To calculate whether an element is selected, we do the + * following: * - * ```html - * <select ng-options="item.subItem as item.label for item in items track by item.id" ng-model="selected"></select> - * ``` - * ```js - * $scope.selected = $scope.items[0].subItem; - * ``` - * - * In both examples, the **`track by`** expression is applied successfully to each `item` in the - * `items` array. Because the selected option has been set programmatically in the controller, the - * **`track by`** expression is also applied to the `ngModel` value. In the first example, the - * `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with - * no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`** - * expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value - * is not matched against any `<option>` and the `<select>` appears as having no selected value. + * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]` + * 2. Apply **`track by`** to the already selected value in `ngModel`. + * In the example: this is not possible as **`track by`** refers to `item.id`, but the selected + * value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to + * a wrong object, the selected element can't be found, `<select>` is always reset to the "not + * selected" option. * * * @param {string} ngModel Assignable angular expression to data-bind to. @@ -41032,8 +39358,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; var value = optionValues[key]; - var locals = getLocals(value, key); - var selectValue = getTrackByValueFn(value, locals); + var locals = getLocals(optionValues[key], key); + var selectValue = getTrackByValueFn(optionValues[key], locals); watchedArray.push(selectValue); // Only need to watch the displayFn if there is a specific label expression @@ -41099,10 +39425,17 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var optionTemplate = document.createElement('option'), optGroupTemplate = document.createElement('optgroup'); - function ngOptionsPostLink(scope, selectElement, attr, ctrls) { + return { + restrict: 'A', + terminal: true, + require: ['select', '?ngModel'], + link: function(scope, selectElement, attr, ctrls) { - var selectCtrl = ctrls[0]; + // if ngModel is not defined, we don't need to do anything var ngModelCtrl = ctrls[1]; + if (!ngModelCtrl) return; + + var selectCtrl = ctrls[0]; var multiple = attr.multiple; // The emptyOption allows the application developer to provide their own custom "empty" @@ -41151,6 +39484,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { unknownOption.remove(); }; + // Update the controller methods for multiple selectable options if (!multiple) { @@ -41158,20 +39492,14 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var option = options.getOptionFromViewValue(value); if (option && !option.disabled) { - // Don't update the option when it is already selected. - // For example, the browser will select the first option by default. In that case, - // most properties are set automatically - except the `selected` attribute, which we - // set always - if (selectElement[0].value !== option.selectValue) { removeUnknownOption(); removeEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; + option.element.setAttribute('selected', 'selected'); } - - option.element.setAttribute('selected', 'selected'); } else { if (value === null || providedEmptyOption) { removeUnknownOption(); @@ -41231,7 +39559,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { forEach(selectedValues, function(value) { var option = options.selectValueMap[value]; - if (option && !option.disabled) selections.push(options.getViewValueFromOption(option)); + if (!option.disabled) selections.push(options.getViewValueFromOption(option)); }); return selections; @@ -41284,16 +39612,11 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { 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 - // 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. - // This is unfortunately untestable with unit / e2e tests + if (option.value !== element.value) element.value = option.selectValue; if (option.label !== element.label) { element.label = option.label; element.textContent = option.label; } - if (option.value !== element.value) element.value = option.selectValue; } function addOrReuseElement(parent, current, type, templateElement) { @@ -41331,15 +39654,10 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var emptyOption_ = emptyOption && emptyOption[0]; var unknownOption_ = unknownOption && unknownOption[0]; - // We cannot rely on the extracted empty option being the same as the compiled empty option, - // because the compiled empty option might have been replaced by a comment because - // it had an "element" transclusion directive on it (such as ngIf) if (emptyOption_ || unknownOption_) { while (current && (current === emptyOption_ || - current === unknownOption_ || - current.nodeType === NODE_TYPE_COMMENT || - (nodeName_(current) === 'option' && current.value === ''))) { + current === unknownOption_)) { current = current.nextSibling; } } @@ -41368,7 +39686,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var groupElement; var optionElement; - if (isDefined(option.group)) { + if (option.group) { // This option is to live in a group // See if we have already created this group @@ -41429,28 +39747,14 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { // Check to see if the value has changed due to the update to the options if (!ngModelCtrl.$isEmpty(previousValue)) { var nextValue = selectCtrl.readValue(); - var isNotPrimitive = ngOptions.trackBy || multiple; - if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) { + if (ngOptions.trackBy ? !equals(previousValue, nextValue) : previousValue !== nextValue) { ngModelCtrl.$setViewValue(nextValue); ngModelCtrl.$render(); } } } - } - return { - restrict: 'A', - terminal: true, - require: ['select', 'ngModel'], - link: { - pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) { - // Deactivate the SelectController.register method to prevent - // option directives from accidentally registering themselves - // (and unwanted $destroy handlers etc.) - ctrls[0].registerOption = noop; - }, - post: ngOptionsPostLink } }; }]; @@ -41670,7 +39974,7 @@ 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. + // In JS `NaN !== NaN`, so we have to exlicitly check. if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) { watchRemover(); var whenExpFn = whensExpFns[count]; @@ -41715,10 +40019,8 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | * - * <div class="alert alert-info"> - * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. - * This may be useful when, for instance, nesting ngRepeats. - * </div> + * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. + * This may be useful when, for instance, nesting ngRepeats. * * * # Iterating over object properties @@ -41730,44 +40032,32 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * <div ng-repeat="(key, value) in myObj"> ... </div> * ``` * - * However, there are a limitations compared to array iteration: + * You need to be aware that the JavaScript specification does not define the order of keys + * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive + * used to sort the keys alphabetically.) * - * - The JavaScript specification does not define the order of keys - * returned for an object, so Angular 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). + * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser + * when running `for key in myObj`. It seems that 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 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_issues * - * - `ngRepeat` will silently *ignore* object keys starting with `$`, because - * it's a prefix used by Angular 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 if used with one. - * - * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array - * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could + * If this is not desired, the recommended workaround is to convert your object into an array + * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter) * or implement a `$watch` on the object yourself. * * * # 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: + * When the contents of the collection change, `ngRepeat` 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. * * When items are reordered, their respective templates are reordered in the DOM. * - * 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 - * 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. + * By default, `ngRepeat` 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. * * If you do need to repeat duplicate items, you can substitute the default tracking behavior * with your own using the `track by` expression. @@ -41780,7 +40070,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * </div> * ``` * - * You may also use arbitrary expressions in `track by`, including references to custom functions + * You may 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)"> @@ -41788,14 +40078,10 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * </div> * ``` * - * <div class="alert alert-success"> - * If you are working with objects that have an identifier property, you should track + * If you are working with objects that have an identifier property, you can 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> + * JavaScript objects in the collection have been substituted for new ones: * ```html * <div ng-repeat="model in collection track by model.id"> * {{model.name}} @@ -41864,13 +40150,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). * * @animations - * | Animation | Occurs | - * |----------------------------------|-------------------------------------| - * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter | - * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out | - * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered | + * **.enter** - when a new item is added to the list or when an item is revealed after a filter * - * See the example below for defining CSS animations with ngRepeat. + * **.leave** - when an item is removed from the list or when an item is filtered out + * + * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered * * @element ANY * @scope @@ -41924,11 +40208,22 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` . * * @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. - <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true"> + * This example initializes the scope to a list of names and + * then uses `ngRepeat` to display every person: + <example module="ngAnimate" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="repeatController"> + <div ng-init="friends = [ + {name:'John', age:25, gender:'boy'}, + {name:'Jessie', age:30, gender:'girl'}, + {name:'Johanna', age:28, gender:'girl'}, + {name:'Joy', age:15, gender:'girl'}, + {name:'Mary', age:28, gender:'girl'}, + {name:'Peter', age:95, gender:'boy'}, + {name:'Sebastian', age:50, gender:'boy'}, + {name:'Erika', age:27, gender:'girl'}, + {name:'Patrick', age:40, gender:'boy'}, + {name:'Samantha', age:60, gender:'girl'} + ]"> 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"> @@ -41941,22 +40236,6 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, </ul> </div> </file> - <file name="script.js"> - angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) { - $scope.friends = [ - {name:'John', age:25, gender:'boy'}, - {name:'Jessie', age:30, gender:'girl'}, - {name:'Johanna', age:28, gender:'girl'}, - {name:'Joy', age:15, gender:'girl'}, - {name:'Mary', age:28, gender:'girl'}, - {name:'Peter', age:95, gender:'boy'}, - {name:'Sebastian', age:50, gender:'boy'}, - {name:'Erika', age:27, gender:'girl'}, - {name:'Patrick', age:40, gender:'boy'}, - {name:'Samantha', age:60, gender:'girl'} - ]; - }); - </file> <file name="animations.css"> .example-animate-container { background:white; @@ -41967,7 +40246,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, } .animate-repeat { - line-height:30px; + line-height:40px; list-style:none; box-sizing:border-box; } @@ -41975,6 +40254,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, .animate-repeat.ng-move, .animate-repeat.ng-enter, .animate-repeat.ng-leave { + -webkit-transition:all linear 0.5s; transition:all linear 0.5s; } @@ -41989,7 +40269,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, .animate-repeat.ng-move.ng-move-active, .animate-repeat.ng-enter.ng-enter-active { opacity:1; - max-height:30px; + max-height:40px; } </file> <file name="protractor.js" type="protractor"> @@ -42016,7 +40296,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, </file> </example> */ -var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) { +var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); @@ -42051,7 +40331,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani $$tlb: true, compile: function ngRepeatCompile($element, $attr) { var expression = $attr.ngRepeat; - var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); 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*$/); @@ -42146,7 +40426,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani // if object, extract keys, in enumeration order, unsorted collectionKeys = []; for (var itemKey in collection) { - if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { + if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') { collectionKeys.push(itemKey); } } @@ -42215,7 +40495,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockNodes(block.clone), null, previousNode); + $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); @@ -42227,7 +40507,8 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani var endNode = ngRepeatEndComment.cloneNode(false); clone[clone.length++] = endNode; - $animate.enter(clone, null, previousNode); + // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? + $animate.enter(clone, null, jqLite(previousNode)); previousNode = endNode; // 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 @@ -42330,14 +40611,12 @@ 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 + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, 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 | + * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden * * @element ANY * @param {expression} ngShow If the {@link guide/expression expression} is truthy @@ -42372,7 +40651,9 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; background: white; } - .animate-show.ng-hide-add, .animate-show.ng-hide-remove { + .animate-show.ng-hide-add.ng-hide-add-active, + .animate-show.ng-hide-remove.ng-hide-remove-active { + -webkit-transition: all linear 0.5s; transition: all linear 0.5s; } @@ -42496,15 +40777,12 @@ 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 + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, 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 | - * + * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible * * @element ANY * @param {expression} ngHide If the {@link guide/expression expression} is truthy then @@ -42532,6 +40810,7 @@ var ngShowDirective = ['$animate', function($animate) { </file> <file name="animations.css"> .animate-hide { + -webkit-transition: all linear 0.5s; transition: all linear 0.5s; line-height: 20px; opacity: 1; @@ -42666,10 +40945,8 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * </div> * @animations - * | Animation | Occurs | - * |----------------------------------|-------------------------------------| - * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container | - * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM | + * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container + * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM * * @usage * @@ -42732,6 +41009,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { } .animate-switch.ng-animate { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; position:absolute; @@ -42768,7 +41046,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { </file> </example> */ -var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { +var ngSwitchDirective = ['$animate', function($animate) { return { require: 'ngSwitch', @@ -42809,7 +41087,7 @@ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { selectedTransclude.transclude(function(caseElement, selectedScope) { selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; - caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen'); + caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); var block = { clone: caseElement }; selectedElements.push(block); @@ -42852,186 +41130,67 @@ var ngSwitchDefaultDirective = ngDirective({ * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. * - * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name - * as the value of the `ng-transclude` or `ng-transclude-slot` attribute. - * - * 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. + * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. * * @element ANY * - * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty - * or its value is the same as the name of the attribute then the default slot is used. - * * @example - * ### Basic transclusion - * This example demonstrates basic transclusion of content into a component directive. - * <example name="simpleTranscludeExample" module="transcludeExample"> - * <file name="index.html"> - * <script> - * angular.module('transcludeExample', []) - * .directive('pane', function(){ - * return { - * restrict: 'E', - * transclude: true, - * scope: { title:'@' }, - * template: '<div style="border: 1px solid black;">' + - * '<div style="background-color: gray">{{title}}</div>' + - * '<ng-transclude></ng-transclude>' + - * '</div>' - * }; - * }) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.title = 'Lorem Ipsum'; - * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - * }]); - * </script> - * <div ng-controller="ExampleController"> - * <input ng-model="title" aria-label="title"> <br/> - * <textarea ng-model="text" aria-label="text"></textarea> <br/> - * <pane title="{{title}}">{{text}}</pane> - * </div> - * </file> - * <file name="protractor.js" type="protractor"> - * it('should have transcluded', function() { - * var titleElement = element(by.model('title')); - * titleElement.clear(); - * titleElement.sendKeys('TITLE'); - * var textElement = element(by.model('text')); - * textElement.clear(); - * textElement.sendKeys('TEXT'); - * expect(element(by.binding('title')).getText()).toEqual('TITLE'); - * expect(element(by.binding('text')).getText()).toEqual('TEXT'); - * }); - * </file> - * </example> - * - * @example - * ### Transclude fallback content - * This example shows how to use `NgTransclude` with fallback content, that - * is displayed if no transcluded content is provided. - * - * <example module="transcludeFallbackContentExample"> - * <file name="index.html"> - * <script> - * angular.module('transcludeFallbackContentExample', []) - * .directive('myButton', function(){ - * return { - * restrict: 'E', - * transclude: true, - * scope: true, - * template: '<button style="cursor: pointer;">' + - * '<ng-transclude>' + - * '<b style="color: red;">Button1</b>' + - * '</ng-transclude>' + - * '</button>' - * }; - * }); - * </script> - * <!-- fallback button content --> - * <my-button id="fallback"></my-button> - * <!-- modified button content --> - * <my-button id="modified"> - * <i style="color: green;">Button2</i> - * </my-button> - * </file> - * <file name="protractor.js" type="protractor"> - * it('should have different transclude element content', function() { - * expect(element(by.id('fallback')).getText()).toBe('Button1'); - * expect(element(by.id('modified')).getText()).toBe('Button2'); - * }); - * </file> - * </example> + <example module="transcludeExample"> + <file name="index.html"> + <script> + angular.module('transcludeExample', []) + .directive('pane', function(){ + return { + restrict: 'E', + transclude: true, + scope: { title:'@' }, + template: '<div style="border: 1px solid black;">' + + '<div style="background-color: gray">{{title}}</div>' + + '<ng-transclude></ng-transclude>' + + '</div>' + }; + }) + .controller('ExampleController', ['$scope', function($scope) { + $scope.title = 'Lorem Ipsum'; + $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + }]); + </script> + <div ng-controller="ExampleController"> + <input ng-model="title" aria-label="title"> <br/> + <textarea ng-model="text" aria-label="text"></textarea> <br/> + <pane title="{{title}}">{{text}}</pane> + </div> + </file> + <file name="protractor.js" type="protractor"> + it('should have transcluded', function() { + var titleElement = element(by.model('title')); + titleElement.clear(); + titleElement.sendKeys('TITLE'); + var textElement = element(by.model('text')); + textElement.clear(); + textElement.sendKeys('TEXT'); + expect(element(by.binding('title')).getText()).toEqual('TITLE'); + expect(element(by.binding('text')).getText()).toEqual('TEXT'); + }); + </file> + </example> * - * @example - * ### Multi-slot transclusion - * This example demonstrates using multi-slot transclusion in a component directive. - * <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample"> - * <file name="index.html"> - * <style> - * .title, .footer { - * background-color: gray - * } - * </style> - * <div ng-controller="ExampleController"> - * <input ng-model="title" aria-label="title"> <br/> - * <textarea ng-model="text" aria-label="text"></textarea> <br/> - * <pane> - * <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title> - * <pane-body><p>{{text}}</p></pane-body> - * </pane> - * </div> - * </file> - * <file name="app.js"> - * angular.module('multiSlotTranscludeExample', []) - * .directive('pane', function(){ - * return { - * restrict: 'E', - * transclude: { - * 'title': '?paneTitle', - * 'body': 'paneBody', - * 'footer': '?paneFooter' - * }, - * template: '<div style="border: 1px solid black;">' + - * '<div class="title" ng-transclude="title">Fallback Title</div>' + - * '<div ng-transclude="body"></div>' + - * '<div class="footer" ng-transclude="footer">Fallback Footer</div>' + - * '</div>' - * }; - * }) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.title = 'Lorem Ipsum'; - * $scope.link = "https://google.com"; - * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - * }]); - * </file> - * <file name="protractor.js" type="protractor"> - * it('should have transcluded the title and the body', function() { - * var titleElement = element(by.model('title')); - * titleElement.clear(); - * titleElement.sendKeys('TITLE'); - * var textElement = element(by.model('text')); - * textElement.clear(); - * textElement.sendKeys('TEXT'); - * expect(element(by.css('.title')).getText()).toEqual('TITLE'); - * expect(element(by.binding('text')).getText()).toEqual('TEXT'); - * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer'); - * }); - * </file> - * </example> */ -var ngTranscludeMinErr = minErr('ngTransclude'); var ngTranscludeDirective = ngDirective({ restrict: 'EAC', link: function($scope, $element, $attrs, controller, $transclude) { - - if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) { - // If the attribute is of the form: `ng-transclude="ng-transclude"` - // then treat it like the default - $attrs.ngTransclude = ''; - } - - function ngTranscludeCloneAttachFn(clone) { - if (clone.length) { - $element.empty(); - $element.append(clone); - } - } - if (!$transclude) { - throw ngTranscludeMinErr('orphan', + throw minErr('ngTransclude')('orphan', 'Illegal use of ngTransclude directive in the template! ' + 'No parent directive that requires a transclusion found. ' + 'Element: {0}', startingTag($element)); } - // If there is no slot name defined or the slot name is not optional - // then transclude the slot - var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot; - $transclude(ngTranscludeCloneAttachFn, null, slotName); + $transclude(function(clone) { + $element.empty(); + $element.append(clone); + }); } }); @@ -43085,15 +41244,6 @@ var scriptDirective = ['$templateCache', function($templateCache) { 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; - } -} - /** * @ngdoc type * @name select.SelectController @@ -43103,7 +41253,7 @@ function chromeHack(optionElement) { * added `<option>` elements, perhaps by an `ngRepeat` directive. */ var SelectController = - ['$element', '$scope', function($element, $scope) { + ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { var self = this, optionsMap = new HashMap(); @@ -43163,17 +41313,12 @@ var SelectController = // Tell the select control that an option, with the given value, has been added self.addOption = function(value, element) { - // Skip comment nodes, as they only pollute the `optionsMap` - if (element[0].nodeType === NODE_TYPE_COMMENT) return; - assertNotHasOwnProperty(value, '"option value"'); if (value === '') { self.emptyOption = element; } var count = optionsMap.get(value) || 0; optionsMap.put(value, count + 1); - self.ngModelCtrl.$render(); - chromeHack(element); }; // Tell the select control that an option, with the given value, has been removed @@ -43195,39 +41340,6 @@ var SelectController = self.hasOption = function(value) { return !!optionsMap.get(value); }; - - - self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) { - - if (interpolateValueFn) { - // The value attribute is interpolated - var oldVal; - optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { - if (isDefined(oldVal)) { - self.removeOption(oldVal); - } - oldVal = newVal; - self.addOption(newVal, optionElement); - }); - } else if (interpolateTextFn) { - // The text content is interpolated - optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) { - optionAttrs.$set('value', newVal); - if (oldVal !== newVal) { - self.removeOption(oldVal); - } - self.addOption(newVal, optionElement); - }); - } else { - // The value attribute is static - self.addOption(optionAttrs.value, optionElement); - } - - optionElement.on('$destroy', function() { - self.removeOption(optionAttrs.value); - self.ngModelCtrl.$render(); - }); - }; }]; /** @@ -43238,164 +41350,31 @@ var SelectController = * @description * HTML `SELECT` element with angular 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). - * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or - * {@link ngOptions `ngOptions`} directives. - * - * 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. + * 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 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. * - * <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> + * 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` + * directive. * - * 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. + * If the viewValue contains a value that doesn't match any of the options then the control + * will automatically add an "unknown" option, which it then removes when this is resolved. * * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can * 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"> - * 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> - * - * - * @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=} multiple Allows multiple options to be selected. The selected values will be - * bound to the model as an array. - * @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 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 - * 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`}. - * - * @example - * ### Simple `select` elements with static options - * - * <example name="static-select" module="staticSelect"> - * <file name="index.html"> - * <div ng-controller="ExampleController"> - * <form name="myForm"> - * <label for="singleSelect"> Single select: </label><br> - * <select name="singleSelect" ng-model="data.singleSelect"> - * <option value="option-1">Option 1</option> - * <option value="option-2">Option 2</option> - * </select><br> - * - * <label for="singleSelect"> Single select with "not selected" option and dynamic option values: </label><br> - * <select name="singleSelect" id="singleSelect" ng-model="data.singleSelect"> - * <option value="">---Please select---</option> <!-- not selected / blank option --> - * <option value="{{data.option1}}">Option 1</option> <!-- interpolation --> - * <option value="option-2">Option 2</option> - * </select><br> - * <button ng-click="forceUnknownOption()">Force unknown option</button><br> - * <tt>singleSelect = {{data.singleSelect}}</tt> - * - * <hr> - * <label for="multipleSelect"> Multiple select: </label><br> - * <select name="multipleSelect" id="multipleSelect" ng-model="data.multipleSelect" multiple> - * <option value="option-1">Option 1</option> - * <option value="option-2">Option 2</option> - * <option value="option-3">Option 3</option> - * </select><br> - * <tt>multipleSelect = {{data.multipleSelect}}</tt><br/> - * </form> - * </div> - * </file> - * <file name="app.js"> - * angular.module('staticSelect', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.data = { - * singleSelect: null, - * multipleSelect: [], - * option1: 'option-1', - * }; - * - * $scope.forceUnknownOption = function() { - * $scope.data.singleSelect = 'nonsense'; - * }; - * }]); - * </file> - *</example> - * - * ### Using `ngRepeat` to generate `select` options - * <example name="ngrepeat-select" 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"> - * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option> - * </select> - * </form> - * <hr> - * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/> + * 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 explictly 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> - * </file> - * <file name="app.js"> - * angular.module('ngrepeatSelect', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.data = { - * repeatSelect: null, - * availableOptions: [ - * {id: '1', name: 'Option A'}, - * {id: '2', name: 'Option B'}, - * {id: '3', name: 'Option C'} - * ], - * }; - * }]); - * </file> - *</example> - * - * - * ### Using `select` with `ngOptions` and setting a default value - * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples. * - * <example name="select-with-default-values" module="defaultValueSelect"> - * <file name="index.html"> - * <div ng-controller="ExampleController"> - * <form name="myForm"> - * <label for="mySelect">Make a choice:</label> - * <select name="mySelect" id="mySelect" - * ng-options="option.name for option in data.availableOptions track by option.id" - * ng-model="data.selectedOption"></select> - * </form> - * <hr> - * <tt>option = {{data.selectedOption}}</tt><br/> - * </div> - * </file> - * <file name="app.js"> - * angular.module('defaultValueSelect', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.data = { - * availableOptions: [ - * {id: '1', name: 'Option A'}, - * {id: '2', name: 'Option B'}, - * {id: '3', name: 'Option C'} - * ], - * selectedOption: {id: '3', name: 'Option C'} //This sets the default value of the select in the ui - * }; - * }]); - * </file> - *</example> - * - * - * ### Binding `select` to a non-string value via `ngModel` parsing / formatting + * ### Example (binding `select` to a non-string value) * * <example name="select-with-non-string-options" module="nonStringSelect"> * <file name="index.html"> @@ -43440,14 +41419,7 @@ var selectDirective = function() { restrict: 'E', require: ['select', '?ngModel'], controller: SelectController, - priority: 1, - link: { - pre: selectPreLink, - post: selectPostLink - } - }; - - function selectPreLink(scope, element, attr, ctrls) { + link: function(scope, element, attr, ctrls) { // if ngModel is not defined, we don't need to do anything var ngModelCtrl = ctrls[1]; @@ -43457,6 +41429,13 @@ var selectDirective = function() { selectCtrl.ngModelCtrl = ngModelCtrl; + // We delegate rendering to the `writeValue` method, which can be changed + // if the select can have multiple selected values or if the options are being + // generated by `ngOptions` + ngModelCtrl.$render = function() { + selectCtrl.writeValue(ngModelCtrl.$viewValue); + }; + // When the selected item(s) changes we delegate getting the value of the select control // 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` @@ -43510,23 +41489,7 @@ var selectDirective = function() { } } - - function selectPostLink(scope, element, attrs, ctrls) { - // if ngModel is not defined, we don't need to do anything - var ngModelCtrl = ctrls[1]; - if (!ngModelCtrl) return; - - var selectCtrl = ctrls[0]; - - // We delegate rendering to the `writeValue` method, which can be changed - // if the select can have multiple selected values or if the options are being - // generated by `ngOptions`. - // This must be done in the postLink fn to prevent $render to be called before - // all nodes have been linked correctly. - ngModelCtrl.$render = function() { - selectCtrl.writeValue(ngModelCtrl.$viewValue); - }; - } + }; }; @@ -43534,23 +41497,32 @@ var selectDirective = function() { // of dynamically created (and destroyed) option elements to their containing select // directive via its controller. var optionDirective = ['$interpolate', function($interpolate) { + + 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; + } + } + return { restrict: 'E', priority: 100, compile: function(element, attr) { - if (isDefined(attr.value)) { - // If the value attribute is defined, check if it contains an interpolation - var 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); - if (!interpolateTextFn) { + + // If the value attribute is not defined then we fall back to the + // text content of the option element, which may be interpolated + if (isUndefined(attr.value)) { + var interpolateFn = $interpolate(element.text(), true); + if (!interpolateFn) { attr.$set('value', element.text()); } } return function(scope, element, attr) { + // This is an optimization over using ^^ since we don't want to have to search // all the way to the root of the DOM for every single option element var selectCtrlName = '$selectController', @@ -43558,8 +41530,30 @@ var optionDirective = ['$interpolate', function($interpolate) { selectCtrl = parent.data(selectCtrlName) || parent.parent().data(selectCtrlName); // in case we are in optgroup - if (selectCtrl) { - selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn); + // Only update trigger option updates if this is an option within a `select` + // that also has `ngModel` attached + if (selectCtrl && selectCtrl.ngModelCtrl) { + + if (interpolateFn) { + scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { + attr.$set('value', newVal); + if (oldVal !== newVal) { + selectCtrl.removeOption(oldVal); + } + selectCtrl.addOption(newVal, element); + selectCtrl.ngModelCtrl.$render(); + chromeHack(element); + }); + } else { + selectCtrl.addOption(attr.value, element); + selectCtrl.ngModelCtrl.$render(); + chromeHack(element); + } + + element.on('$destroy', function() { + selectCtrl.removeOption(attr.value); + selectCtrl.ngModelCtrl.$render(); + }); } }; } @@ -43571,64 +41565,6 @@ var styleDirective = valueFn({ terminal: false }); -/** - * @ngdoc directive - * @name ngRequired - * - * @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 - * `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. - * - * The validator will set the `required` error key to true if the `required` attribute is set and - * calling {@link ngModel.NgModelController#$isEmpty `NgModelController.$isEmpty`} with the - * {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} returns `true`. For example, the - * `$isEmpty()` implementation for `input[text]` checks the length of the `$viewValue`. When developing - * custom controls, `$isEmpty()` can be overwritten to account for a $viewValue that is not string-based. - * - * @example - * <example name="ngRequiredDirective" module="ngRequiredExample"> - * <file name="index.html"> - * <script> - * angular.module('ngRequiredExample', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.required = true; - * }]); - * </script> - * <div ng-controller="ExampleController"> - * <form name="form"> - * <label for="required">Toggle required: </label> - * <input type="checkbox" ng-model="required" id="required" /> - * <br> - * <label for="input">This input must be filled if `required` is true: </label> - * <input type="text" ng-model="model" id="input" name="input" ng-required="required" /><br> - * <hr> - * required error set? = <code>{{form.input.$error.required}}</code><br> - * model = <code>{{model}}</code> - * </form> - * </div> - * </file> - * <file name="protractor.js" type="protractor"> - var required = element(by.binding('form.input.$error.required')); - var model = element(by.binding('model')); - var input = element(by.id('input')); - - it('should set the required error', function() { - expect(required.getText()).toContain('true'); - - input.sendKeys('123'); - expect(required.getText()).not.toContain('true'); - expect(model.getText()).toContain('123'); - }); - * </file> - * </example> - */ var requiredDirective = function() { return { restrict: 'A', @@ -43648,81 +41584,7 @@ var requiredDirective = function() { }; }; -/** - * @ngdoc directive - * @name ngPattern - * - * @description - * - * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. - * 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$')`. - * - * <div class="alert alert-info"> - * **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. - * </div> - * - * <div class="alert alert-info"> - * **Note:** This directive is also added when the plain `pattern` attribute is used, with two - * differences: - * <ol> - * <li> - * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is - * not available. - * </li> - * <li> - * The `ngPattern` attribute must be an expression, while the `pattern` value must be - * interpolated. - * </li> - * </ol> - * </div> - * - * @example - * <example name="ngPatternDirective" module="ngPatternExample"> - * <file name="index.html"> - * <script> - * angular.module('ngPatternExample', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.regex = '\\d+'; - * }]); - * </script> - * <div ng-controller="ExampleController"> - * <form name="form"> - * <label for="regex">Set a pattern (regex string): </label> - * <input type="text" ng-model="regex" id="regex" /> - * <br> - * <label for="input">This input is restricted by the current pattern: </label> - * <input type="text" ng-model="model" id="input" name="input" ng-pattern="regex" /><br> - * <hr> - * input valid? = <code>{{form.input.$valid}}</code><br> - * model = <code>{{model}}</code> - * </form> - * </div> - * </file> - * <file name="protractor.js" type="protractor"> - var model = element(by.binding('model')); - var input = element(by.id('input')); - - it('should validate the input with the default pattern', function() { - input.sendKeys('aaa'); - expect(model.getText()).not.toContain('aaa'); - input.clear().then(function() { - input.sendKeys('123'); - expect(model.getText()).toContain('123'); - }); - }); - * </file> - * </example> - */ var patternDirective = function() { return { restrict: 'A', @@ -43746,80 +41608,14 @@ var patternDirective = function() { 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(value) { + return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; } }; }; -/** - * @ngdoc directive - * @name ngMaxlength - * - * @description - * - * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. - * 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 - * `ngMaxlength` attribute value. - * - * <div class="alert alert-info"> - * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two - * differences: - * <ol> - * <li> - * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint - * validation is not available. - * </li> - * <li> - * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be - * interpolated. - * </li> - * </ol> - * </div> - * - * @example - * <example name="ngMaxlengthDirective" module="ngMaxlengthExample"> - * <file name="index.html"> - * <script> - * angular.module('ngMaxlengthExample', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.maxlength = 5; - * }]); - * </script> - * <div ng-controller="ExampleController"> - * <form name="form"> - * <label for="maxlength">Set a maxlength: </label> - * <input type="number" ng-model="maxlength" id="maxlength" /> - * <br> - * <label for="input">This input is restricted by the current maxlength: </label> - * <input type="text" ng-model="model" id="input" name="input" ng-maxlength="maxlength" /><br> - * <hr> - * input valid? = <code>{{form.input.$valid}}</code><br> - * model = <code>{{model}}</code> - * </form> - * </div> - * </file> - * <file name="protractor.js" type="protractor"> - var model = element(by.binding('model')); - var input = element(by.id('input')); - - it('should validate the input with the default maxlength', function() { - input.sendKeys('abcdef'); - expect(model.getText()).not.toContain('abcdef'); - input.clear().then(function() { - input.sendKeys('abcde'); - expect(model.getText()).toContain('abcde'); - }); - }); - * </file> - * </example> - */ var maxlengthDirective = function() { return { restrict: 'A', @@ -43840,70 +41636,6 @@ var maxlengthDirective = function() { }; }; -/** - * @ngdoc directive - * @name ngMinlength - * - * @description - * - * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. - * 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 - * `ngMinlength` attribute value. - * - * <div class="alert alert-info"> - * **Note:** This directive is also added when the plain `minlength` attribute is used, with two - * differences: - * <ol> - * <li> - * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint - * validation is not available. - * </li> - * <li> - * The `ngMinlength` value must be an expression, while the `minlength` value must be - * interpolated. - * </li> - * </ol> - * </div> - * - * @example - * <example name="ngMinlengthDirective" module="ngMinlengthExample"> - * <file name="index.html"> - * <script> - * angular.module('ngMinlengthExample', []) - * .controller('ExampleController', ['$scope', function($scope) { - * $scope.minlength = 3; - * }]); - * </script> - * <div ng-controller="ExampleController"> - * <form name="form"> - * <label for="minlength">Set a minlength: </label> - * <input type="number" ng-model="minlength" id="minlength" /> - * <br> - * <label for="input">This input is restricted by the current minlength: </label> - * <input type="text" ng-model="model" id="input" name="input" ng-minlength="minlength" /><br> - * <hr> - * input valid? = <code>{{form.input.$valid}}</code><br> - * model = <code>{{model}}</code> - * </form> - * </div> - * </file> - * <file name="protractor.js" type="protractor"> - var model = element(by.binding('model')); - var input = element(by.id('input')); - - it('should validate the input with the default minlength', function() { - input.sendKeys('ab'); - expect(model.getText()).not.toContain('ab'); - - input.sendKeys('abc'); - expect(model.getText()).toContain('abc'); - }); - * </file> - * </example> - */ var minlengthDirective = function() { return { restrict: 'A', @@ -43923,162 +41655,17 @@ var minlengthDirective = function() { }; }; -if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... - if (window.console) { + if (window.angular.bootstrap) { + //AngularJS is already loaded, so we can return here... console.log('WARNING: Tried to load angular more than once.'); + return; } - return; -} - -//try to bind to jquery now so that one can write jqLite(document).ready() -//but we will rebind on bootstrap again. -bindJQuery(); - -publishExternalAPI(angular); - -angular.module("ngLocale", [], ["$provide", function($provide) { -var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; -function getDecimals(n) { - n = n + ''; - var i = n.indexOf('.'); - return (i == -1) ? 0 : n.length - i - 1; -} -function getVF(n, opt_precision) { - var v = opt_precision; + //try to bind to jquery now so that one can write jqLite(document).ready() + //but we will rebind on bootstrap again. + bindJQuery(); - if (undefined === v) { - v = Math.min(getDecimals(n), 3); - } - - var base = Math.pow(10, v); - var f = ((n * base) | 0) % base; - return {v: v, f: f}; -} - -$provide.value("$locale", { - "DATETIME_FORMATS": { - "AMPMS": [ - "AM", - "PM" - ], - "DAY": [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" - ], - "ERANAMES": [ - "Before Christ", - "Anno Domini" - ], - "ERAS": [ - "BC", - "AD" - ], - "FIRSTDAYOFWEEK": 6, - "MONTH": [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" - ], - "SHORTDAY": [ - "Sun", - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat" - ], - "SHORTMONTH": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ], - "STANDALONEMONTH": [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" - ], - "WEEKENDRANGE": [ - 5, - 6 - ], - "fullDate": "EEEE, MMMM d, y", - "longDate": "MMMM d, y", - "medium": "MMM d, y h:mm:ss a", - "mediumDate": "MMM d, y", - "mediumTime": "h:mm:ss a", - "short": "M/d/yy h:mm a", - "shortDate": "M/d/yy", - "shortTime": "h:mm a" - }, - "NUMBER_FORMATS": { - "CURRENCY_SYM": "$", - "DECIMAL_SEP": ".", - "GROUP_SEP": ",", - "PATTERNS": [ - { - "gSize": 3, - "lgSize": 3, - "maxFrac": 3, - "minFrac": 0, - "minInt": 1, - "negPre": "-", - "negSuf": "", - "posPre": "", - "posSuf": "" - }, - { - "gSize": 3, - "lgSize": 3, - "maxFrac": 2, - "minFrac": 2, - "minInt": 1, - "negPre": "-\u00a4", - "negSuf": "", - "posPre": "\u00a4", - "posSuf": "" - } - ] - }, - "id": "en-us", - "localeID": "en_US", - "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;} -}); -}]); + publishExternalAPI(angular); jqLite(document).ready(function() { angularInit(document, bootstrap); @@ -44086,7 +41673,7 @@ $provide.value("$locale", { })(window, document); -!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>'); +!window.angular.$$csp() && 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>'); /*! * ionic.bundle.js is a concatenation of: * ionic.js, angular.js, angular-animate.js, @@ -44095,15 +41682,14 @@ $provide.value("$locale", { */ /** - * @license AngularJS v1.5.3 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.3 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; /* jshint ignore:start */ var noop = angular.noop; -var copy = angular.copy; var extend = angular.extend; var jqLite = angular.element; var forEach = angular.forEach; @@ -44118,63 +41704,13 @@ var isElement = angular.isElement; var ELEMENT_NODE = 1; var COMMENT_NODE = 8; -var ADD_CLASS_SUFFIX = '-add'; -var REMOVE_CLASS_SUFFIX = '-remove'; -var EVENT_CLASS_PREFIX = 'ng-'; -var ACTIVE_CLASS_SUFFIX = '-active'; -var PREPARE_CLASS_SUFFIX = '-prepare'; - var NG_ANIMATE_CLASSNAME = 'ng-animate'; var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; -// Detect proper transitionend/animationend event names. -var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; - -// If unprefixed events are not supported but webkit-prefixed are, use the latter. -// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. -// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` -// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. -// Register both events in case `window.onanimationend` is not supported because of that, -// do the same for `transitionend` as Safari is likely to exhibit similar behavior. -// 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 (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) { - CSS_PREFIX = '-webkit-'; - TRANSITION_PROP = 'WebkitTransition'; - TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; -} else { - TRANSITION_PROP = 'transition'; - TRANSITIONEND_EVENT = 'transitionend'; -} - -if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { - CSS_PREFIX = '-webkit-'; - ANIMATION_PROP = 'WebkitAnimation'; - ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; -} else { - ANIMATION_PROP = 'animation'; - ANIMATIONEND_EVENT = 'animationend'; -} - -var DURATION_KEY = 'Duration'; -var PROPERTY_KEY = 'Property'; -var DELAY_KEY = 'Delay'; -var TIMING_KEY = 'TimingFunction'; -var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; -var ANIMATION_PLAYSTATE_KEY = 'PlayState'; -var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; - -var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; -var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; -var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; -var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; - var isPromiseLike = function(p) { return p && p.then ? true : false; -}; +} -var ngMinErr = angular.$$minErr('ng'); function assertArg(arg, name, reason) { if (!arg) { throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); @@ -44319,29 +41855,13 @@ function applyAnimationToStyles(element, options) { } } -function mergeAnimationDetails(element, oldAnimation, newAnimation) { - var target = oldAnimation.options || {}; - var newOptions = newAnimation.options || {}; - +function mergeAnimationOptions(element, target, newOptions) { var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || ''); var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || ''); var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove); - if (newOptions.preparationClasses) { - target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses); - delete newOptions.preparationClasses; - } - - // noop is basically when there is no callback; otherwise something has been set - var realDomOperation = target.domOperation !== noop ? target.domOperation : null; - extend(target, newOptions); - // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this. - if (realDomOperation) { - target.domOperation = realDomOperation; - } - if (classes.addClass) { target.addClass = classes.addClass; } else { @@ -44354,9 +41874,6 @@ function mergeAnimationDetails(element, oldAnimation, newAnimation) { target.removeClass = null; } - oldAnimation.addClass = target.addClass; - oldAnimation.removeClass = target.removeClass; - return target; } @@ -44422,77 +41939,20 @@ function getDomNode(element) { return (element instanceof angular.element) ? element[0] : element; } -function applyGeneratedPreparationClasses(element, event, options) { - var classes = ''; - if (event) { - classes = pendClasses(event, EVENT_CLASS_PREFIX, true); - } - if (options.addClass) { - classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX)); - } - if (options.removeClass) { - classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX)); - } - if (classes.length) { - options.preparationClasses = classes; - element.addClass(classes); - } -} - -function clearGeneratedClasses(element, options) { - if (options.preparationClasses) { - element.removeClass(options.preparationClasses); - options.preparationClasses = null; - } - if (options.activeClasses) { - element.removeClass(options.activeClasses); - options.activeClasses = null; - } -} - -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; - applyInlineStyle(node, [key, value]); - return [key, value]; -} - -function applyInlineStyle(node, styleTuple) { - var prop = styleTuple[0]; - var value = styleTuple[1]; - node.style[prop] = value; -} - -function concatWithSpace(a,b) { - if (!a) return b; - if (!b) return a; - return a + ' ' + b; -} - var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { - var queue, cancelFn; + var tickQueue = []; + var cancelFn; function scheduler(tasks) { // we make a copy since RAFScheduler mutates the state // of the passed in array variable and this would be difficult // to track down on the outside code - queue = queue.concat(tasks); + tickQueue.push([].concat(tasks)); nextTick(); } - queue = scheduler.queue = []; - /* waitUntilQuiet does two things: - * 1. It will run the FINAL `fn` value only when an uncanceled RAF has passed through + * 1. It will run the FINAL `fn` value only when an uncancelled RAF has passed through * 2. It will delay the next wave of tasks from running until the quiet `fn` has run. * * The motivation here is that animation code can request more time from the scheduler @@ -44512,12 +41972,17 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { return scheduler; function nextTick() { - if (!queue.length) return; + if (!tickQueue.length) return; - var items = queue.shift(); - for (var i = 0; i < items.length; i++) { - items[i](); + var updatedQueue = []; + for (var i = 0; i < tickQueue.length; i++) { + var innerQueue = tickQueue[i]; + runNextTask(innerQueue); + if (innerQueue.length) { + updatedQueue.push(innerQueue); + } } + tickQueue = updatedQueue; if (!cancelFn) { $$rAF(function() { @@ -44525,109 +41990,27 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { }); } } -}]; - -/** - * @ngdoc directive - * @name ngAnimateChildren - * @restrict AE - * @element ANY - * - * @description - * - * ngAnimateChildren allows you to specify that children of this element should animate even if any - * 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). - * - * - * @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. - * - * @example - * <example module="ngAnimateChildren" name="ngAnimateChildren" deps="angular-animate.js" animations="true"> - <file name="index.html"> - <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> - <div ng-animate-children="{{main.animateChildren}}"> - <div ng-if="main.enterElement" class="container"> - List of items: - <div ng-repeat="item in [0, 1, 2, 3]" class="item">Item {{item}}</div> - </div> - </div> - </div> - </file> - <file name="animations.css"> - - .container.ng-enter, - .container.ng-leave { - transition: all ease 1.5s; - } - - .container.ng-enter, - .container.ng-leave-active { - opacity: 0; - } - - .container.ng-leave, - .container.ng-enter-active { - opacity: 1; - } - - .item { - background: firebrick; - color: #FFF; - margin-bottom: 10px; - } - - .item.ng-enter, - .item.ng-leave { - transition: transform 1.5s ease; - } - - .item.ng-enter { - transform: translateX(50px); - } - .item.ng-enter-active { - transform: translateX(0); - } - </file> - <file name="script.js"> - angular.module('ngAnimateChildren', ['ngAnimate']) - .controller('mainController', function() { - this.animateChildren = false; - this.enterElement = false; - }); - </file> - </example> - */ -var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { - return { - link: function(scope, element, attrs) { - var val = attrs.ngAnimateChildren; - if (angular.isString(val) && val.length === 0) { //empty attribute - element.data(NG_ANIMATE_CHILDREN_DATA, true); - } else { - // Interpolate and set the value, so that it is available to - // animations that run right after compilation - setData($interpolate(val)(scope)); - attrs.$observe('ngAnimateChildren', setData); - } + function runNextTask(tasks) { + var nextTask = tasks.shift(); + nextTask(); + } +}]; - function setData(value) { +var $$AnimateChildrenDirective = [function() { + return function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN_DATA, true); + } else { + attrs.$observe('ngAnimateChildren', function(value) { value = value === 'on' || value === 'true'; element.data(NG_ANIMATE_CHILDREN_DATA, value); - } + }); } }; }]; -var ANIMATE_TIMER_KEY = '$$animateCss'; - /** * @ngdoc service * @name $animateCss @@ -44793,7 +42176,7 @@ var ANIMATE_TIMER_KEY = '$$animateCss'; * ``` * * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. - * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been + * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and stlyes may have been * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties * and that changing them will not reconfigure the parameters of the animation. * @@ -44812,10 +42195,8 @@ var ANIMATE_TIMER_KEY = '$$animateCss'; * * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) - * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and - * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). - * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). + * * `transition` - The raw CSS transition style that will be used (e.g. `1s linear all`). * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. @@ -44830,23 +42211,63 @@ var ANIMATE_TIMER_KEY = '$$animateCss'; * * `stagger` - A numeric time value representing the delay between successively animated elements * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a - * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) - * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occurring on the classes being added and removed.) - * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once - * the animation is closed. This is useful for when the styles are used purely for the sake of - * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation). - * By default this value is set to `false`. + * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) + * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.) * * @return {object} an object with start and end methods and details about the animation. * * * `start` - The method to start the animation. This will return a `Promise` when called. * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. */ -var ONE_SECOND = 1000; -var BASE_TEN = 10; +// Detect proper transitionend/animationend event names. +var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; + +// If unprefixed events are not supported but webkit-prefixed are, use the latter. +// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. +// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` +// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. +// Register both events in case `window.onanimationend` is not supported because of that, +// do the same for `transitionend` as Safari is likely to exhibit similar behavior. +// 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 === undefined && window.onwebkittransitionend !== undefined) { + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; +} else { + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; +} + +if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { + CSS_PREFIX = '-webkit-'; + ANIMATION_PROP = 'WebkitAnimation'; + ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; +} else { + ANIMATION_PROP = 'animation'; + ANIMATIONEND_EVENT = 'animationend'; +} + +var DURATION_KEY = 'Duration'; +var PROPERTY_KEY = 'Property'; +var DELAY_KEY = 'Delay'; +var TIMING_KEY = 'TimingFunction'; +var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; +var ANIMATION_PLAYSTATE_KEY = 'PlayState'; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; +var ONE_SECOND = 1000; +var BASE_TEN = 10; + +var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; + +var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; +var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; + +var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; +var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; var DETECT_CSS_PROPERTIES = { transitionDuration: TRANSITION_DURATION_PROP, @@ -44864,15 +42285,6 @@ var DETECT_STAGGER_CSS_PROPERTIES = { animationDelay: ANIMATION_DELAY_PROP }; -function getCssKeyframeDurationStyle(duration) { - return [ANIMATION_DURATION_PROP, duration + 's']; -} - -function getCssDelayStyle(delay, isKeyframeAnimation) { - var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; - return [prop, delay + 's']; -} - function computeCssStyles($window, element, properties) { var styles = Object.create(null); var detectedStyles = $window.getComputedStyle(element) || {}; @@ -44887,7 +42299,7 @@ function computeCssStyles($window, element, properties) { } // by setting this to null in the event that the delay is not set or is set directly as 0 - // then we can still allow for negative values to be used later on and not mistake this + // then we can still allow for zegative values to be used later on and not mistake this // value for being greater than any other negative value. if (val === 0) { val = null; @@ -44929,6 +42341,37 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) { return [style, value]; } +function getCssKeyframeDurationStyle(duration) { + return [ANIMATION_DURATION_PROP, duration + 's']; +} + +function getCssDelayStyle(delay, isKeyframeAnimation) { + var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; + return [prop, delay + 's']; +} + +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; + applyInlineStyle(node, [key, value]); + return [key, value]; +} + +function applyInlineStyle(node, styleTuple) { + var prop = styleTuple[0]; + var value = styleTuple[1]; + node.style[prop] = value; +} + function createLocalCacheLookup() { var cache = Object.create(null); return { @@ -44956,31 +42399,14 @@ function createLocalCacheLookup() { }; } -// 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. -// We make use of `isDefined` here since an empty string -// or null value (which is what getPropertyValue will return -// for a non-existing style) will still be marked as a valid -// value for the style (a falsy value implies that the style -// is to be removed at the end of the animation). If we had a simple -// "OR" statement then it would not be enough to catch that. -function registerRestorableStyles(backup, node, properties) { - forEach(properties, function(prop) { - backup[prop] = isDefined(backup[prop]) - ? backup[prop] - : node.style.getPropertyValue(prop); - }); -} - var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var gcsLookup = createLocalCacheLookup(); var gcsStaggerLookup = createLocalCacheLookup(); this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', - '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', + '$document', '$sniffer', '$$rAFScheduler', function($window, $$jqLite, $$AnimateRunner, $timeout, - $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { + $document, $sniffer, $$rAFScheduler) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); @@ -45003,7 +42429,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } // 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. + // because we're keeping an interal tally of how many duplicate animations are detected. gcsLookup.put(cacheKey, timings); return timings; } @@ -45037,7 +42463,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return stagger || {}; } - var cancelLastRAFRequest; + var bod = getDomNode($document).body; var rafWaitQueue = []; function waitUntilQuiet(callback) { rafWaitQueue.push(callback); @@ -45045,19 +42471,27 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { gcsLookup.flush(); gcsStaggerLookup.flush(); - // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. - // PLEASE EXAMINE THE `$$forceReflow` service to understand why. - var pageWidth = $$forceReflow(); + //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. + var width = bod.offsetWidth + 1; // we use a for loop to ensure that if the queue is changed // during this looping then it will consider new requests for (var i = 0; i < rafWaitQueue.length; i++) { - rafWaitQueue[i](pageWidth); + rafWaitQueue[i](width); } rafWaitQueue.length = 0; }); } + return init; + function computeTimings(node, className, cacheKey) { var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); var aD = timings.animationDelay; @@ -45072,24 +42506,14 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return timings; } - return function init(element, initialOptions) { - // all of the animation functions should create - // a copy of the options data, however, if a - // parent service has already created a copy then - // we should stick to using that - var options = initialOptions || {}; - if (!options.$$prepared) { - options = prepareAnimationOptions(copy(options)); - } - - var restoreStyles = {}; + function init(element, options) { var node = getDomNode(element); - if (!node - || !node.parentNode - || !$$animateQueue.enabled()) { + if (!node || !node.parentNode) { return closeAndReturnNoopAnimator(); } + options = prepareAnimationOptions(options); + var temporaryStyles = []; var classes = element.attr('class'); var styles = packageStyles(options); @@ -45102,8 +42526,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var maxDelayTime; var maxDuration; var maxDurationTime; - var startTime; - var events = []; if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { return closeAndReturnNoopAnimator(); @@ -45118,20 +42540,20 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var addRemoveClassName = ''; if (isStructural) { - structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); + structuralClassName = pendClasses(method, 'ng-', true); } else if (method) { structuralClassName = method; } if (options.addClass) { - addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); + addRemoveClassName += pendClasses(options.addClass, '-add'); } if (options.removeClass) { if (addRemoveClassName.length) { addRemoveClassName += ' '; } - addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); + addRemoveClassName += pendClasses(options.removeClass, '-remove'); } // there may be a situation where a structural animation is combined together @@ -45142,11 +42564,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // there actually is a detected transition or keyframe animation if (options.applyClassesEarly && addRemoveClassName.length) { applyAnimationClasses(element, options); + addRemoveClassName = ''; } - var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); - var fullClassName = classes + ' ' + preparationClasses; - var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); + var setupClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); + var fullClassName = classes + ' ' + setupClasses; + var activeClasses = pendClasses(setupClasses, '-active'); var hasToStyles = styles.to && Object.keys(styles.to).length > 0; var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; @@ -45155,7 +42578,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // unless there a is raw keyframe value that is applied to the element. if (!containsKeyframeAnimation && !hasToStyles - && !preparationClasses) { + && !setupClasses) { return closeAndReturnNoopAnimator(); } @@ -45170,12 +42593,10 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }; } else { cacheKey = gcsHashFn(node, fullClassName); - stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); + stagger = computeCachedCssStaggerStyles(node, setupClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); } - if (!options.$$skipPreparationClasses) { - $$jqLite.addClass(element, preparationClasses); - } + $$jqLite.addClass(element, setupClasses); var applyOnlyDuration; @@ -45214,7 +42635,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // transition delay to allow for the transition to naturally do it's thing. The beauty here is // 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) { + if (isFirst) { blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); } @@ -45256,23 +42677,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return closeAndReturnNoopAnimator(); } - if (options.delay != null) { - var delayStyle; - 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); - } - - if (flags.applyTransitionDelay) { - temporaryStyles.push(getCssDelayStyle(delayStyle)); - } - - if (flags.applyAnimationDelay) { - temporaryStyles.push(getCssDelayStyle(delayStyle, true)); - } - } - // we need to recalculate the delay value since we used a pre-emptive negative // delay value and the delay value is required for the final event checking. This // property will ensure that this will happen after the RAF phase has passed. @@ -45289,19 +42693,13 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { stagger.animationDuration === 0; } - if (options.from) { - if (options.cleanupStyles) { - registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); - } - applyAnimationFromStyles(element, options); - } - - if (flags.blockTransition || flags.blockKeyframeAnimation) { - applyBlocking(maxDuration); - } else if (!options.skipBlocking) { + applyAnimationFromStyles(element, options); + if (!flags.blockTransition) { blockTransitions(node, false); } + applyBlocking(maxDuration); + // TODO(matsko): for 1.5 change this code to have an animator object for better debugging return { $$willAnimate: true, @@ -45343,9 +42741,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { animationClosed = true; animationPaused = false; - if (!options.$$skipPreparationClasses) { - $$jqLite.removeClass(element, preparationClasses); - } + $$jqLite.removeClass(element, setupClasses); $$jqLite.removeClass(element, activeClasses); blockKeyframeAnimations(node, false); @@ -45361,13 +42757,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); applyAnimationStyles(element, options); - if (Object.keys(restoreStyles).length) { - forEach(restoreStyles, function(value, prop) { - value ? node.style.setProperty(prop, value) - : node.style.removeProperty(prop); - }); - } - // the reason why we have this option is to allow a synchronous closing callback // that is fired as SOON as the animation ends (when the CSS is removed) or if // the animation never takes off at all. A good example is a leave animation since @@ -45377,18 +42766,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { options.onDone(); } - if (events && events.length) { - // Remove the transitionend / animationend listener(s) - element.off(events.join(' '), onAnimationProgress); - } - - //Cancel the fallback closing timeout and remove the timer data - var animationTimerData = element.data(ANIMATE_TIMER_KEY); - if (animationTimerData) { - $timeout.cancel(animationTimerData[0].timer); - element.removeData(ANIMATE_TIMER_KEY); - } - // if the preparation function fails then the promise is not setup if (runner) { runner.complete(!rejected); @@ -45411,8 +42788,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { cancel: cancelFn }); - // should flush the cache animation - waitUntilQuiet(noop); close(); return { @@ -45424,33 +42799,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }; } - function onAnimationProgress(event) { - event.stopPropagation(); - var ev = event.originalEvent || event; - - // 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(); - - /* Firefox (or possibly just Gecko) likes to not round values up - * when a ms measurement is used for the animation */ - var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); - - /* $manualTimeStamp is a mocked timeStamp value which is set - * within browserTrigger(). This is only here so that tests can - * mock animations properly. Real events fallback to event.timeStamp, - * or, if they don't, then a timeStamp is automatically created for them. - * We're checking to see if the timeStamp surpasses the expected delay, - * but we're using elapsedTime instead of the timeStamp on the 2nd - * pre-condition since animationPauseds sometimes close off early */ - if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { - // we set this flag to ensure that if the transition is paused then, when resumed, - // the animation will automatically close itself since transitions cannot be paused. - animationCompleted = true; - close(); - } - } - function start() { if (animationClosed) return; if (!node.parentNode) { @@ -45458,6 +42806,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return; } + var startTime, events = []; + // even though we only pause keyframe animations here the pause flag // will still happen when transitions are used. Only the transition will // not be paused since that is not possible. If the animation ends when @@ -45477,9 +42827,9 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } }; - // checking the stagger duration prevents an accidentally cascade of the CSS delay style + // checking the stagger duration prevents an accidently cascade of the CSS delay style // being inherited from the parent. If the transition duration is zero then we can safely - // rely that the delay value is an intentional stagger delay style. + // rely that the delay value is an intential stagger delay style. var maxStagger = itemIndex > 0 && ((timings.transitionDuration && stagger.transitionDuration === 0) || (timings.animationDuration && stagger.animationDuration === 0)) @@ -45518,7 +42868,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { $$jqLite.addClass(element, activeClasses); if (flags.recalculateTimingStyles) { - fullClassName = node.className + ' ' + preparationClasses; + fullClassName = node.className + ' ' + setupClasses; cacheKey = gcsHashFn(node, fullClassName); timings = computeTimings(node, fullClassName, cacheKey); @@ -45535,16 +42885,27 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { flags.hasAnimations = timings.animationDuration > 0; } - if (flags.applyAnimationDelay) { + if (flags.applyTransitionDelay || flags.applyAnimationDelay) { relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) ? parseFloat(options.delay) : relativeDelay; maxDelay = Math.max(relativeDelay, 0); - timings.animationDelay = relativeDelay; - delayStyle = getCssDelayStyle(relativeDelay, true); - temporaryStyles.push(delayStyle); - node.style[delayStyle[0]] = delayStyle[1]; + + var delayStyle; + if (flags.applyTransitionDelay) { + timings.transitionDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } + + if (flags.applyAnimationDelay) { + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } } maxDelayTime = maxDelay * ONE_SECOND; @@ -45573,58 +42934,44 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } startTime = Date.now(); - var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; - var endTime = startTime + timerTime; - - var animationsData = element.data(ANIMATE_TIMER_KEY) || []; - var setupFallbackTimer = true; - if (animationsData.length) { - var currentTimerData = animationsData[0]; - setupFallbackTimer = endTime > currentTimerData.expectedEndTime; - if (setupFallbackTimer) { - $timeout.cancel(currentTimerData.timer); - } else { - animationsData.push(close); - } - } + element.on(events.join(' '), onAnimationProgress); + $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime); - if (setupFallbackTimer) { - var timer = $timeout(onAnimationExpired, timerTime, false); - animationsData[0] = { - timer: timer, - expectedEndTime: endTime - }; - animationsData.push(close); - element.data(ANIMATE_TIMER_KEY, animationsData); - } - - if (events.length) { - element.on(events.join(' '), onAnimationProgress); - } - - if (options.to) { - if (options.cleanupStyles) { - registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); - } - applyAnimationToStyles(element, options); - } + applyAnimationToStyles(element, options); } function onAnimationExpired() { - var animationsData = element.data(ANIMATE_TIMER_KEY); - - // this will be false in the event that the element was - // removed from the DOM (via a leave animation or something - // similar) - if (animationsData) { - for (var i = 1; i < animationsData.length; i++) { - animationsData[i](); - } - element.removeData(ANIMATE_TIMER_KEY); + // although an expired animation is a failed animation, getting to + // this outcome is very easy if the CSS code screws up. Therefore we + // should still continue normally as if the animation completed correctly. + close(); + } + + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animations sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); } } } - }; + } }]; }]; @@ -45637,27 +42984,16 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; - function isDocumentFragment(node) { - return node.parentNode && node.parentNode.nodeType === 11; - } - - this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', - function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$document', '$sniffer', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $document, $sniffer) { // only browsers that support these properties can render animations if (!$sniffer.animations && !$sniffer.transitions) return noop; - var bodyNode = $document[0].body; + var bodyNode = getDomNode($document).body; var rootNode = getDomNode($rootElement); - var rootBodyElement = jqLite( - // this is to avoid using something that exists outside of the body - // we also special case the doc fragment case because our unit test code - // appends the $rootElement to the body after the app has been bootstrapped - isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode - ); - - var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode); return function initDriverFn(animationDetails) { return animationDetails.from && animationDetails.to @@ -45752,7 +43088,7 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro var coords = getDomNode(anchor).getBoundingClientRect(); // we iterate directly since safari messes up and doesn't return - // all the keys for the coords object when iterated + // all the keys for the coods object when iterated forEach(['width','height','top','left'], function(key) { var value = coords[key]; switch (key) { @@ -45809,8 +43145,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro } function prepareFromToAnchorAnimation(from, to, classes, anchors) { - var fromAnimation = prepareRegularAnimation(from, noop); - var toAnimation = prepareRegularAnimation(to, noop); + var fromAnimation = prepareRegularAnimation(from); + var toAnimation = prepareRegularAnimation(to); var anchorAnimations = []; forEach(anchors, function(anchor) { @@ -45866,23 +43202,19 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro var options = animationDetails.options || {}; if (animationDetails.structural) { - options.event = animationDetails.event; - options.structural = true; - options.applyClassesEarly = true; + // structural animations ensure that the CSS classes are always applied + // before the detection starts. + options.structural = options.applyClassesEarly = true; // we special case the leave animation since we want to ensure that // the element is removed as soon as the animation is over. Otherwise // a flicker might appear or the element may not be removed at all - if (animationDetails.event === 'leave') { + options.event = animationDetails.event; + if (options.event === 'leave') { options.onDone = options.domOperation; } - } - - // We assign the preparationClasses as the actual animation event since - // the internals of $animateCss will just suffix the event token values - // with `-active` to trigger the animation. - if (options.preparationClasses) { - options.event = concatWithSpace(options.event, options.preparationClasses); + } else { + options.event = null; } var animator = $animateCss(element, options); @@ -45901,14 +43233,12 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro // by the time... var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { - this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', - function($injector, $$AnimateRunner, $$jqLite) { + this.$get = ['$injector', '$$AnimateRunner', '$$rAFMutex', '$$jqLite', + function($injector, $$AnimateRunner, $$rAFMutex, $$jqLite) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); // $animateJs(element, 'enter'); return function(element, event, classes, options) { - var animationClosed = false; - // the `classes` argument is optional and if it is not used // then the classes will be resolved from the element's className // property as well as options.addClass/options.removeClass. @@ -45961,32 +43291,8 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); } - function close() { - animationClosed = true; - applyOptions(); - applyAnimationStyles(element, options); - } - - var runner; - return { - $$willAnimate: true, - end: function() { - if (runner) { - runner.end(); - } else { - close(); - runner = new $$AnimateRunner(); - runner.complete(true); - } - return runner; - }, start: function() { - if (runner) { - return runner; - } - - runner = new $$AnimateRunner(); var closeActiveAnimations; var chain = []; @@ -46011,7 +43317,8 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { }); } - runner.setHost({ + var animationClosed = false; + var runner = new $$AnimateRunner({ end: function() { endAnimations(); }, @@ -46024,7 +43331,9 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { return runner; function onComplete(success) { - close(success); + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); runner.complete(success); } @@ -46244,7 +43553,6 @@ var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; - var ONE_SPACE = ' '; var rules = this.rules = { skip: [], @@ -46252,50 +43560,28 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { join: [] }; - function makeTruthyCssClassMap(classString) { - if (!classString) { - return null; - } - - var keys = classString.split(ONE_SPACE); - var map = Object.create(null); - - forEach(keys, function(key) { - map[key] = true; - }); - return map; - } - - function hasMatchingClasses(newClassString, currentClassString) { - if (newClassString && currentClassString) { - var currentClassMap = makeTruthyCssClassMap(currentClassString); - return newClassString.split(ONE_SPACE).some(function(className) { - return currentClassMap[className]; - }); - } - } - function isAllowed(ruleType, element, currentAnimation, previousAnimation) { return rules[ruleType].some(function(fn) { return fn(element, currentAnimation, previousAnimation); }); } - function hasAnimationClasses(animation, and) { - var a = (animation.addClass || '').length > 0; - var b = (animation.removeClass || '').length > 0; + function hasAnimationClasses(options, and) { + options = options || {}; + var a = (options.addClass || '').length > 0; + var b = (options.removeClass || '').length > 0; return and ? a && b : a || b; } rules.join.push(function(element, newAnimation, currentAnimation) { // if the new animation is class-based then we can just tack that on - return !newAnimation.structural && hasAnimationClasses(newAnimation); + return !newAnimation.structural && hasAnimationClasses(newAnimation.options); }); rules.skip.push(function(element, 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); + return !newAnimation.structural && !hasAnimationClasses(newAnimation.options); }); rules.skip.push(function(element, newAnimation, currentAnimation) { @@ -46305,8 +43591,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); rules.skip.push(function(element, 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; + // if there is a current animation then skip the class-based animation + return currentAnimation.structural && !newAnimation.structural; }); rules.cancel.push(function(element, newAnimation, currentAnimation) { @@ -46321,50 +43607,22 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); rules.cancel.push(function(element, 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 - - if (currentAnimation.structural) return false; - - var nA = newAnimation.addClass; - var nR = newAnimation.removeClass; - var cA = currentAnimation.addClass; - var cR = currentAnimation.removeClass; + var nO = newAnimation.options; + var cO = currentAnimation.options; - // early detection to save the global CPU shortage :) - if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { - return false; - } - - return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); + // if the exact same CSS class is added/removed then it's safe to cancel it + return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass); }); this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', - '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', function($$rAF, $rootScope, $rootElement, $document, $$HashMap, - $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + $$animation, $$AnimateRunner, $templateRequest, $$jqLite) { var activeAnimationsLookup = new $$HashMap(); var disabledElementsLookup = new $$HashMap(); - var animationsEnabled = null; - function postDigestTaskFactory() { - var postDigestCalled = false; - return function(fn) { - // we only issue a call to postDigest before - // it has first passed. This prevents any callbacks - // from not firing once the animation has completed - // since it will be out of the digest cycle. - if (postDigestCalled) { - fn(); - } else { - $rootScope.$$postDigest(function() { - postDigestCalled = true; - fn(); - }); - } - }; - } + var animationsEnabled = null; // Wait until all directive and route-related templates are downloaded and // compiled. The $templateRequest.totalPendingRequests variable keeps track of @@ -46395,6 +43653,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } ); + var bodyElement = jqLite($document[0].body); + var callbackRegistry = {}; // remember that the classNameFilter is set during the provider/config @@ -46408,28 +43668,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - function normalizeAnimationDetails(element, animation) { - return mergeAnimationDetails(element, animation, {}); + function normalizeAnimationOptions(element, options) { + return mergeAnimationOptions(element, options, {}); } - // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. - var contains = Node.prototype.contains || function(arg) { - // jshint bitwise: false - return this === arg || !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true - }; - - function findCallbacks(parent, element, event) { + function findCallbacks(element, event) { var targetNode = getDomNode(element); - var targetParentNode = getDomNode(parent); var matches = []; var entries = callbackRegistry[event]; if (entries) { forEach(entries, function(entry) { - if (contains.call(entry.node, targetNode)) { - matches.push(entry.callback); - } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { + if (entry.node.contains(targetNode)) { matches.push(entry.callback); } }); @@ -46438,7 +43688,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return matches; } - var $animate = { + function triggerCallback(event, element, phase, data) { + $$rAF(function() { + forEach(findCallbacks(element, event), function(callback) { + callback(element, phase, data); + }); + }); + } + + return { on: function(event, container, callback) { var node = extractElementNode(container); callbackRegistry[event] = callbackRegistry[event] || []; @@ -46446,11 +43704,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { node: node, callback: callback }); - - // Remove the callback when the element is removed from the DOM - jqLite(container).on('$destroy', function() { - $animate.off(event, container, callback); - }); }, off: function(event, container, callback) { @@ -46509,7 +43762,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { bool = !recordExists; } else { // (element, bool) - Element setter - disabledElementsLookup.put(node, !bool); + bool = !!bool; + if (!bool) { + disabledElementsLookup.put(node, true); + } else if (recordExists) { + disabledElementsLookup.remove(node); + } } } } @@ -46518,14 +43776,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } }; - return $animate; - - function queueAnimation(element, 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); - + function queueAnimation(element, event, options) { var node, parent; element = stripCommentsFromElement(element); if (element) { @@ -46539,25 +43790,22 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // These methods will become available after the digest has passed var runner = new $$AnimateRunner(); - // this is used to trigger callbacks in postDigest mode - var runInNextPostDigestOrNow = postDigestTaskFactory(); + // 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; + } if (isArray(options.addClass)) { options.addClass = options.addClass.join(' '); } - if (options.addClass && !isString(options.addClass)) { - options.addClass = null; - } - if (isArray(options.removeClass)) { options.removeClass = options.removeClass.join(' '); } - if (options.removeClass && !isString(options.removeClass)) { - options.removeClass = null; - } - if (options.from && !isObject(options.from)) { options.from = null; } @@ -46566,14 +43814,6 @@ 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)) { close(); @@ -46585,9 +43825,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // 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 - // 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 || $document[0].hidden || disabledElementsLookup.get(node); + var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node); var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; var hasExistingAnimation = !!existingAnimation.state; @@ -46610,8 +43848,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { structural: isStructural, element: element, event: event, - addClass: options.addClass, - removeClass: options.removeClass, close: close, options: options, runner: runner @@ -46624,10 +43860,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { close(); return runner; } else { - mergeAnimationDetails(element, existingAnimation, newAnimation); + mergeAnimationOptions(element, existingAnimation.options, options); return existingAnimation.runner; } } + var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); if (cancelAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { @@ -46641,10 +43878,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // method which will call the runner methods in async. existingAnimation.close(); } else { - // this will merge the new animation options into existing animation options - mergeAnimationDetails(element, existingAnimation, newAnimation); - - return existingAnimation.runner; + // this will merge the existing animation options into this new follow-up animation + mergeAnimationOptions(element, newAnimation.options, existingAnimation.options); } } else { // a joined animation means that this animation will take over the existing one @@ -46653,23 +43888,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); if (joinAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { - normalizeAnimationDetails(element, newAnimation); + normalizeAnimationOptions(element, options); } else { - applyGeneratedPreparationClasses(element, isStructural ? event : null, options); - event = newAnimation.event = existingAnimation.event; - options = mergeAnimationDetails(element, existingAnimation, newAnimation); - - //we return the same runner since only the option values of this animation will - //be fed into the `existingAnimation`. - return existingAnimation.runner; + options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options); + return runner; } } } } else { // normalization in this case means that it removes redundant CSS classes that // already exist (addClass) or do not exist (removeClass) on the element - normalizeAnimationDetails(element, newAnimation); + normalizeAnimationOptions(element, options); } // when the options are merged and cleaned up we may end up not having to do @@ -46679,7 +43909,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { if (!isValidAnimation) { // animate (from/to) can be quickly checked first, otherwise we check if any classes are present isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) - || hasAnimationClasses(newAnimation); + || hasAnimationClasses(newAnimation.options); } if (!isValidAnimation) { @@ -46688,6 +43918,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return runner; } + if (isStructural) { + closeParentClassBasedAnimations(parent); + } + // the counter keeps track of cancelled animations var counter = (existingAnimation.counter || 0) + 1; newAnimation.counter = counter; @@ -46709,7 +43943,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var isValidAnimation = parentElement.length > 0 && (animationDetails.event === 'animate' || animationDetails.structural - || hasAnimationClasses(animationDetails)); + || hasAnimationClasses(animationDetails.options)); // this means that the previous animation was cancelled // even if the follow-up animation is the same event @@ -46741,13 +43975,16 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // this combined multiple class to addClass / removeClass into a setClass event // so long as a structural event did not take over the animation - event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) + event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true) ? 'setClass' : animationDetails.event; + if (animationDetails.structural) { + closeParentClassBasedAnimations(parentElement); + } + markElementAnimationState(element, RUNNING_STATE); var realRunner = $$animation(element, event, animationDetails.options); - realRunner.done(function(status) { close(!status); var animationDetails = activeAnimationsLookup.get(node); @@ -46766,25 +44003,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return runner; function notifyProgress(runner, event, phase, data) { - runInNextPostDigestOrNow(function() { - var callbacks = findCallbacks(parent, element, event); - if (callbacks.length) { - // do not optimize this call here to RAF because - // we don't know how heavy the callback code here will - // be and if this code is buffered then this can - // lead to a performance regression. - $$rAF(function() { - forEach(callbacks, function(callback) { - callback(element, phase, data); - }); - }); - } - }); + triggerCallback(event, element, phase, data); runner.progress(event, phase, data); } function close(reject) { // jshint ignore:line - clearGeneratedClasses(element, options); applyAnimationClasses(element, options); applyAnimationStyles(element, options); options.domOperation(); @@ -46798,15 +44021,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { forEach(children, function(child) { var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); var animationDetails = activeAnimationsLookup.get(child); - if (animationDetails) { - switch (state) { - case RUNNING_STATE: - animationDetails.runner.end(); - /* falls through */ - case PRE_DIGEST_STATE: + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: + if (animationDetails) { activeAnimationsLookup.remove(child); - break; - } + } + break; } }); } @@ -46821,61 +44044,67 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); } - /** - * This fn returns false if any of the following is true: - * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed - * b) a parent element has an ongoing structural animation, and animateChildren is false - * c) the element is not a child of the body - * d) the element is not a child of the $rootElement - */ + function closeParentClassBasedAnimations(startingElement) { + var parentNode = getDomNode(startingElement); + do { + if (!parentNode || parentNode.nodeType !== ELEMENT_NODE) break; + + var animationDetails = activeAnimationsLookup.get(parentNode); + if (animationDetails) { + examineParentAnimation(parentNode, animationDetails); + } + + parentNode = parentNode.parentNode; + } while (true); + + // since animations are detected from CSS classes, we need to flush all parent + // class-based animations so that the parent classes are all present for child + // animations to properly function (otherwise any CSS selectors may not work) + function examineParentAnimation(node, animationDetails) { + // enter/leave/move always have priority + if (animationDetails.structural || !hasAnimationClasses(animationDetails.options)) return; + + if (animationDetails.state === RUNNING_STATE) { + animationDetails.runner.end(); + } + clearElementAnimationState(node); + } + } + 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); + var bodyElementDetected = false; + var rootElementDetected = false; var parentAnimationDetected = false; var animateChildren; - var elementDisabled = disabledElementsLookup.get(getDomNode(element)); - var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + var parentHost = element.data(NG_ANIMATE_PIN_DATA); if (parentHost) { parentElement = parentHost; } - parentElement = getDomNode(parentElement); - - while (parentElement) { + while (parentElement && parentElement.length) { if (!rootElementDetected) { // angular 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); } - if (parentElement.nodeType !== ELEMENT_NODE) { + var parentNode = parentElement[0]; + 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); - - if (parentElementDisabled === 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) { - elementDisabled = false; - } - parentAnimationDetected = details.structural; + parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode); } if (isUndefined(animateChildren) || animateChildren === true) { - var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA); if (isDefined(value)) { animateChildren = value; } @@ -46884,32 +44113,28 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // there is no need to continue traversing at this point if (parentAnimationDetected && animateChildren === false) break; + if (!rootElementDetected) { + // angular 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); + if (!rootElementDetected) { + parentHost = parentElement.data(NG_ANIMATE_PIN_DATA); + if (parentHost) { + parentElement = parentHost; + } + } + } + if (!bodyElementDetected) { - // we also need to ensure that the element is or will be a part of the body element + // we also need to ensure that the element is or will be apart of the body element // otherwise it is pointless to even issue an animation to be rendered bodyElementDetected = isMatchingElement(parentElement, bodyElement); } - if (bodyElementDetected && rootElementDetected) { - // 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 (parentHost) { - // The pin target element becomes the next parent element - parentElement = getDomNode(parentHost); - continue; - } - } - - parentElement = parentElement.parentNode; + parentElement = parentElement.parent(); } - var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; + var allowAnimation = !parentAnimationDetected || animateChildren; return allowAnimation && rootElementDetected && bodyElementDetected; } @@ -46929,112 +44154,184 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }]; }]; -var $$AnimationProvider = ['$animateProvider', function($animateProvider) { - var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; +var $$rAFMutexFactory = ['$$rAF', function($$rAF) { + return function() { + var passed = false; + $$rAF(function() { + passed = true; + }); + return function(fn) { + passed ? fn() : $$rAF(fn); + }; + }; +}]; - var drivers = this.drivers = []; +var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; - var RUNNER_STORAGE_KEY = '$$animationRunner'; + AnimateRunner.chain = function(chain, callback) { + var index = 0; - function setRunner(element, runner) { - element.data(RUNNER_STORAGE_KEY, runner); - } + next(); + function next() { + if (index === chain.length) { + callback(true); + return; + } - function removeRunner(element) { - element.removeData(RUNNER_STORAGE_KEY); - } + chain[index](function(response) { + if (response === false) { + callback(false); + return; + } + index++; + next(); + }); + } + }; - function getRunner(element) { - return element.data(RUNNER_STORAGE_KEY); + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + + function AnimateRunner(host) { + this.setHost(host); + + this._doneCallbacks = []; + this._runInAnimationFrame = $$rAFMutex(); + this._state = 0; } - this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', - function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, - var animationQueue = []; - var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, - function sortAnimations(animations) { - var tree = { children: [] }; - var i, lookup = new $$HashMap(); - - // this is done first beforehand so that the hashmap - // 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] = { - domNode: animation.domNode, - fn: animation.fn, - children: [] + progress: noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + status === false ? reject() : resolve(); + }); }); } + return this.promise; + }, - for (i = 0; i < animations.length; i++) { - processNode(animations[i]); + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); + }, + + 'catch': function(handler) { + return this.getPromise()['catch'](handler); + }, + + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); } + }, - return flatten(tree); + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, - function processNode(entry) { - if (entry.processed) return entry; - entry.processed = true; + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, - var elementNode = entry.domNode; - var parentNode = elementNode.parentNode; - lookup.put(elementNode, entry); + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, - var parentEntry; - while (parentNode) { - parentEntry = lookup.get(parentNode); - if (parentEntry) { - if (!parentEntry.processed) { - parentEntry = processNode(parentEntry); - } - break; - } - parentNode = parentNode.parentNode; - } + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._runInAnimationFrame(function() { + self._resolve(response); + }); + } + }, - (parentEntry || tree).children.push(entry); - return entry; + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; } + } + }; - function flatten(tree) { - var result = []; - var queue = []; - var i; + return AnimateRunner; +}]; - for (i = 0; i < tree.children.length; i++) { - queue.push(tree.children[i]); - } +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; - var remainingLevelEntries = queue.length; - var nextLevelEntries = 0; - var row = []; + var drivers = this.drivers = []; - for (i = 0; i < queue.length; i++) { - var entry = queue[i]; - if (remainingLevelEntries <= 0) { - remainingLevelEntries = nextLevelEntries; - nextLevelEntries = 0; - result.push(row); - row = []; - } - row.push(entry.fn); - entry.children.forEach(function(childEntry) { - nextLevelEntries++; - queue.push(childEntry); - }); - remainingLevelEntries--; - } + var RUNNER_STORAGE_KEY = '$$animationRunner'; - if (row.length) { - result.push(row); - } + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } - return result; - } - } + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); + } + + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } + + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$rAFScheduler) { + + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + var totalPendingClassBasedAnimations = 0; + var totalActiveClassBasedAnimations = 0; + var classBasedAnimationsQueue = []; // TODO(matsko): document the signature in a better way return function(element, event, options) { @@ -47064,10 +44361,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { options.tempClasses = null; } - var prepareClassName; - if (isStructural) { - prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; - $$jqLite.addClass(element, prepareClassName); + var classBasedIndex; + if (!isStructural) { + classBasedIndex = totalPendingClassBasedAnimations; + totalPendingClassBasedAnimations += 1; } animationQueue.push({ @@ -47076,6 +44373,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { element: element, classes: classes, event: event, + classBasedIndex: classBasedIndex, structural: isStructural, options: options, beforeStart: beforeStart, @@ -47090,6 +44388,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { if (animationQueue.length > 1) return runner; $rootScope.$$postDigest(function() { + totalActiveClassBasedAnimations = totalPendingClassBasedAnimations; + totalPendingClassBasedAnimations = 0; + classBasedAnimationsQueue.length = 0; + var animations = []; forEach(animationQueue, function(entry) { // the element was destroyed early on which removed the runner @@ -47097,58 +44399,67 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { // at all and it already has been closed due to destruction. if (getRunner(entry.element)) { animations.push(entry); - } else { - entry.close(); } }); // now any future animations will be in another postDigest animationQueue.length = 0; - var groupedAnimations = groupAnimations(animations); - var toBeSortedAnimations = []; - - forEach(groupedAnimations, function(animationEntry) { - toBeSortedAnimations.push({ - domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), - fn: function triggerAnimationStart() { - // 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 - ? (animationEntry.from.element || animationEntry.to.element) - : animationEntry.element; - - if (getRunner(targetElement)) { - var operation = invokeFirstDriver(animationEntry); - if (operation) { - startAnimationFn = operation.start; - } - } + forEach(groupAnimations(animations), function(animationEntry) { + if (animationEntry.structural) { + triggerAnimationStart(); + } else { + classBasedAnimationsQueue.push({ + node: getDomNode(animationEntry.element), + fn: triggerAnimationStart + }); - if (!startAnimationFn) { - closeFn(); - } else { - var animationRunner = startAnimationFn(); - animationRunner.done(function(status) { - closeFn(!status); - }); - updateAnimationRunners(animationEntry, animationRunner); + if (animationEntry.classBasedIndex === totalActiveClassBasedAnimations - 1) { + // 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. + classBasedAnimationsQueue = classBasedAnimationsQueue.sort(function(a,b) { + return b.node.contains(a.node); + }).map(function(entry) { + return entry.fn; + }); + + $$rAFScheduler(classBasedAnimationsQueue); + } + } + + function triggerAnimationStart() { + // 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 + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement) && getDomNode(targetElement).parentNode) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; } } - }); - }); - // 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)); + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } + } + }); }); return runner; @@ -47234,7 +44545,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { }; // the anchor animations require that the from and to elements both have at least - // one shared CSS class which effectively marries the two elements together to use + // one shared CSS class which effictively marries the two elements together to use // the same animation driver and to properly sequence the anchor animation. if (group.classes.length) { preparedAnimations.push(group); @@ -47292,10 +44603,6 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { if (tempClasses) { $$jqLite.addClass(element, tempClasses); } - if (prepareClassName) { - $$jqLite.removeClass(element, prepareClassName); - prepareClassName = null; - } } function updateAnimationRunners(animation, newRunner) { @@ -47337,127 +44644,12 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { }]; }]; -/** - * @ngdoc directive - * @name ngAnimateSwap - * @restrict A - * @scope - * - * @description - * - * ngAnimateSwap is a animation-oriented directive that allows for the container to - * be removed and entered in whenever the associated expression changes. A - * common usecase for this directive is a rotating banner or slider component which - * contains one image being present at a time. When the active image changes - * then the old image will perform a `leave` animation and the new element - * will be inserted via an `enter` animation. - * - * @animations - * | Animation | Occurs | - * |----------------------------------|--------------------------------------| - * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | - * | {@link ng.$animate#leave leave} | when the old element is removed from the DOM | - * - * @example - * <example name="ngAnimateSwap-directive" module="ngAnimateSwapExample" - * deps="angular-animate.js" - * animations="true" fixBase="true"> - * <file name="index.html"> - * <div class="container" ng-controller="AppCtrl"> - * <div ng-animate-swap="number" class="cell swap-animation" ng-class="colorClass(number)"> - * {{ number }} - * </div> - * </div> - * </file> - * <file name="script.js"> - * angular.module('ngAnimateSwapExample', ['ngAnimate']) - * .controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) { - * $scope.number = 0; - * $interval(function() { - * $scope.number++; - * }, 1000); - * - * var colors = ['red','blue','green','yellow','orange']; - * $scope.colorClass = function(number) { - * return colors[number % colors.length]; - * }; - * }]); - * </file> - * <file name="animations.css"> - * .container { - * height:250px; - * width:250px; - * position:relative; - * overflow:hidden; - * border:2px solid black; - * } - * .container .cell { - * font-size:150px; - * text-align:center; - * line-height:250px; - * position:absolute; - * top:0; - * left:0; - * right:0; - * border-bottom:2px solid black; - * } - * .swap-animation.ng-enter, .swap-animation.ng-leave { - * transition:0.5s linear all; - * } - * .swap-animation.ng-enter { - * top:-250px; - * } - * .swap-animation.ng-enter-active { - * top:0px; - * } - * .swap-animation.ng-leave { - * top:0px; - * } - * .swap-animation.ng-leave-active { - * top:250px; - * } - * .red { background:red; } - * .green { background:green; } - * .blue { background:blue; } - * .yellow { background:yellow; } - * .orange { background:orange; } - * </file> - * </example> - */ -var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) { - return { - restrict: 'A', - transclude: 'element', - terminal: true, - priority: 600, // we use 600 here to ensure that the directive is caught before others - link: function(scope, $element, attrs, ctrl, $transclude) { - var previousElement, previousScope; - scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) { - if (previousElement) { - $animate.leave(previousElement); - } - if (previousScope) { - previousScope.$destroy(); - previousScope = null; - } - if (value || value === 0) { - previousScope = scope.$new(); - $transclude(previousScope, function(element) { - previousElement = element; - $animate.enter(element, null, $element); - }); - } - }); - } - }; -}]; - /* global angularAnimateModule: true, - ngAnimateSwapDirective, - $$AnimateAsyncRunFactory, + $$rAFMutexFactory, $$rAFSchedulerFactory, $$AnimateChildrenDirective, + $$AnimateRunnerFactory, $$AnimateQueueProvider, $$AnimationProvider, $AnimateCssProvider, @@ -47472,7 +44664,7 @@ 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. + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` then the animation hooks are enabled for an Angular app. * * <div doc-module-components="ngAnimate"></div> * @@ -47505,7 +44697,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * 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. * - * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: + * The example below shows how an `enter` animation can be made possible on a element using `ng-if`: * * ```html * <div ng-if="bool" class="fade"> @@ -47640,8 +44832,8 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * /* this will have a 100ms delay between each successive leave animation */ * transition-delay: 0.1s; * - * /* As of 1.4.4, this must always be set: it signals ngAnimate - * to not accidentally inherit a delay property from another CSS class */ + * /* in case the stagger doesn't work then the duration value + * must be set to 0 to avoid an accidental CSS inheritance */ * transition-duration: 0s; * } * .my-animation.ng-enter.ng-enter-active { @@ -47706,39 +44898,11 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * the CSS class once an animation has completed.) * * - * ### The `ng-[event]-prepare` class - * - * This is a special class that can be used to prevent unwanted flickering / flash of content before - * the actual animation starts. The class is added as soon as an animation is initialized, but removed - * before the actual animation starts (after waiting for a $digest). - * It is also only added for *structural* animations (`enter`, `move`, and `leave`). - * - * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` - * into elements that have class-based animations such as `ngClass`. - * - * ```html - * <div ng-class="{red: myProp}"> - * <div ng-class="{blue: myProp}"> - * <div class="message" ng-if="myProp"></div> - * </div> - * </div> - * ``` - * - * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. - * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: - * - * ```css - * .message.ng-enter-prepare { - * opacity: 0; - * } - * - * ``` - * * ## 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 * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the - * `module.animation()` module function we can register the animation. + * `module.animation()` module function we can register the ainmation. * * Let's see an example of a enter/leave animation using `ngRepeat`: * @@ -47770,7 +44934,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * jQuery(element).fadeOut(1000, doneFn); * } * } - * }]); + * }] * ``` * * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as @@ -47801,7 +44965,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * // do some cool animation and call the doneFn * } * } - * }]); + * }] * ``` * * ## CSS + JS Animations Together @@ -47823,7 +44987,7 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * jQuery(element).slideIn(1000, doneFn); * } * } - * }]); + * }] * ``` * * ```css @@ -47843,15 +45007,16 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * ```js * myModule.animation('.slide', ['$animateCss', function($animateCss) { * return { - * enter: function(element) { + * enter: function(element, doneFn) { * // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. - * return $animateCss(element, { + * var runner = $animateCss(element, { * event: 'enter', * structural: true - * }); + * }).start(); +* runner.done(doneFn); * } * } - * }]); + * }] * ``` * * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. @@ -47863,17 +45028,18 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * ```js * myModule.animation('.slide', ['$animateCss', function($animateCss) { * return { - * enter: function(element) { - * return $animateCss(element, { + * enter: function(element, doneFn) { + * var runner = $animateCss(element, { * event: 'enter', - * structural: true, * addClass: 'maroon-setting', * from: { height:0 }, * to: { height: 200 } - * }); + * }).start(); + * + * runner.done(doneFn); * } * } - * }]); + * }] * ``` * * Now we can fill in the rest via our transition CSS code: @@ -48151,6 +45317,31 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * possible be sure to visit the {@link ng.$animate $animate service API page}. * * + * ### Preventing Collisions With Third Party Libraries + * + * Some third-party frameworks place animation duration defaults across many element or className + * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which + * is expecting actual animations on these elements and has to wait for their completion. + * + * You can prevent this unwanted behavior by using a prefix on all your animation classes: + * + * ```css + * /* prefixed with animate- */ + * .animate-fade-add.animate-fade-add-active { + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * You then configure `$animate` to enforce this prefix: + * + * ```js + * $animateProvider.classNameFilter(/animate-/); + * ``` + * + * This also may provide your application with a speed boost since only specific elements containing CSS class prefix + * will be evaluated for animation when any DOM changes occur in the application. + * * ## Callbacks and Promises * * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger @@ -48190,14 +45381,16 @@ var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $root * @description * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. * - * Click here {@link ng.$animate to learn more about animations with `$animate`}. + * Click here {@link ng.$animate $animate to learn more about animations with `$animate`}. */ angular.module('ngAnimate', []) - .directive('ngAnimateSwap', ngAnimateSwapDirective) - .directive('ngAnimateChildren', $$AnimateChildrenDirective) + + .factory('$$rAFMutex', $$rAFMutexFactory) .factory('$$rAFScheduler', $$rAFSchedulerFactory) + .factory('$$AnimateRunner', $$AnimateRunnerFactory) + .provider('$$animateQueue', $$AnimateQueueProvider) .provider('$$animation', $$AnimationProvider) @@ -48218,8 +45411,8 @@ angular.module('ngAnimate', []) */ /** - * @license AngularJS v1.5.3 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.3 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -48252,23 +45445,36 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); * See {@link ngSanitize.$sanitize `$sanitize`} for usage. */ +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + /** * @ngdoc service * @name $sanitize * @kind function * * @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 - * 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 input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. The input may also contain SVG markup. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. * * @param {string} html HTML input. * @returns {string} Sanitized HTML. @@ -48354,70 +45560,16 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); </file> </example> */ - - -/** - * @ngdoc provider - * @name $sanitizeProvider - * - * @description - * Creates and configures {@link $sanitize} instance. - */ function $SanitizeProvider() { - var svgEnabled = false; - this.$get = ['$$sanitizeUri', function($$sanitizeUri) { - if (svgEnabled) { - angular.extend(validElements, svgElements); - } return function(html) { var buf = []; htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); })); return buf.join(''); }; }]; - - - /** - * @ngdoc method - * @name $sanitizeProvider#enableSvg - * @kind function - * - * @description - * Enables a subset of svg to be supported by the sanitizer. - * - * <div class="alert alert-warning"> - * <p>By enabling this setting without taking other precautions, you might expose your - * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned - * outside of the containing element and be rendered over other elements on the page (e.g. a login - * link). Such behavior can then result in phishing incidents.</p> - * - * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg - * tags within the sanitized content:</p> - * - * <br> - * - * <pre><code> - * .rootOfTheIncludedContent svg { - * overflow: hidden !important; - * } - * </code></pre> - * </div> - * - * @param {boolean=} regexp New regexp to whitelist urls with. - * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called - * without an argument or self for chaining otherwise. - */ - this.enableSvg = function(enableSvg) { - if (angular.isDefined(enableSvg)) { - svgEnabled = enableSvg; - return this; - } else { - return svgEnabled; - } - }; } function sanitizeText(chars) { @@ -48429,9 +45581,18 @@ function sanitizeText(chars) { // Regular Expressions for parsing tags and attributes -var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, +var START_TAG_REGEXP = + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^</, + BEGING_END_TAGE_REGEXP = /^<\//, + COMMENT_REGEXP = /<!--(.*?)-->/g, + DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, + CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, + 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 @@ -48440,23 +45601,23 @@ var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, // 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 = makeMap("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 = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); // Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," + +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("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")); + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); // Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," + +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("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")); @@ -48464,23 +45625,24 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,a // 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," + +var svgElements = makeMap("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"); + "radialGradient,rect,stop,svg,switch,text,title,tspan,use"); -// Blocked Elements (will be stripped) -var blockedElements = toMap("script,style"); +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); var validElements = angular.extend({}, voidElements, blockElements, inlineElements, - optionalEndTagElements); + optionalEndTagElements, + svgElements); //Attributes that have href and hence need to be sanitized -var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); -var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + +var htmlAttrs = makeMap('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,' + @@ -48488,7 +45650,7 @@ var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacin // 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 = makeMap('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,' + @@ -48509,7 +45671,7 @@ var validAttrs = angular.extend({}, svgAttrs, htmlAttrs); -function toMap(str, lowercaseKeys) { +function makeMap(str, lowercaseKeys) { var obj = {}, items = str.split(','), i; for (i = 0; i < items.length; i++) { obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; @@ -48517,32 +45679,11 @@ function toMap(str, lowercaseKeys) { 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"); - } - 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); - } -})(window); /** * @example * htmlParser(htmlString, { - * start: function(tag, attrs) {}, + * start: function(tag, attrs, unary) {}, * end: function(tag) {}, * chars: function(text) {}, * comment: function(text) {} @@ -48552,74 +45693,169 @@ var inertBodyElement; * @param {object} handler */ function htmlParser(html, handler) { - if (html === null || html === undefined) { - html = ''; - } else if (typeof html !== 'string') { - html = '' + html; + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } } - inertBodyElement.innerHTML = html; + var index, chars, match, stack = [], last = html, text; + stack.last = function() { return stack[stack.length - 1]; }; - //mXSS protection - var mXSSAttempts = 5; - do { - if (mXSSAttempts === 0) { - throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); - } - mXSSAttempts--; + while (html) { + text = ''; + chars = true; - // strip custom-namespaced attributes on IE<=11 - if (document.documentMode <= 11) { - stripCustomNsAttrs(inertBodyElement); - } - html = inertBodyElement.innerHTML; //trigger mXSS - inertBodyElement.innerHTML = html; - } while (html !== inertBodyElement.innerHTML); + // Make sure we're not in a script or style element + if (!stack.last() || !specialElements[stack.last()]) { - var node = inertBodyElement.firstChild; - while (node) { - switch (node.nodeType) { - case 1: // ELEMENT_NODE - handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes)); - break; - case 3: // TEXT NODE - handler.chars(node.textContent); - break; - } + // Comment + if (html.indexOf("<!--") === 0) { + // comments containing -- are not allowed unless they terminate the comment + index = html.indexOf("--", 4); - var nextNode; - if (!(nextNode = node.firstChild)) { - if (node.nodeType == 1) { - handler.end(node.nodeName.toLowerCase()); - } - nextNode = node.nextSibling; - if (!nextNode) { - while (nextNode == null) { - node = node.parentNode; - if (node === inertBodyElement) break; - nextNode = node.nextSibling; - if (node.nodeType == 1) { - handler.end(node.nodeName.toLowerCase()); + if (index >= 0 && html.lastIndexOf("-->", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); + chars = false; + } + // DOCTYPE + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); + + if (match) { + html = html.replace(match[0], ''); + chars = false; + } + // end tag + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); + + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); + chars = false; + } + + // start tag + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); } + chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); } } + + if (chars) { + index = html.indexOf("<"); + + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); + + if (handler.chars) handler.chars(decodeEntities(text)); + } + + } else { + // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. + html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text) { + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars(decodeEntities(text)); + + return ""; + }); + + parseEndTag("", stack.last()); + } + + if (html == last) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); } - node = nextNode; + last = html; } - while (node = inertBodyElement.firstChild) { - inertBodyElement.removeChild(node); + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = angular.lowercase(tagName); + if (blockElements[tagName]) { + while (stack.last() && inlineElements[stack.last()]) { + parseEndTag("", stack.last()); + } + } + + if (optionalEndTagElements[tagName] && stack.last() == tagName) { + parseEndTag("", tagName); + } + + unary = voidElements[tagName] || !!unary; + + if (!unary) { + stack.push(tagName); + } + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start(tagName, attrs, unary); } -} -function attrToMap(attrs) { - var map = {}; - for (var i = 0, ii = attrs.length; i < ii; i++) { - var attr = attrs[i]; - map[attr.name] = attr.value; + function parseEndTag(tag, tagName) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if (tagName) { + // Find the closest opened tag of the same type + for (pos = stack.length - 1; pos >= 0; pos--) { + if (stack[pos] == tagName) break; + } + } + + if (pos >= 0) { + // Close all the open elements, up the stack + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[i]); + + // Remove the open elements from the stack + stack.length = pos; + } } - return map; } +var hiddenPre=document.createElement("pre"); +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + hiddenPre.innerHTML = value.replace(/</g,"<"); + // innerText depends on styling as it doesn't display hidden elements. + // Therefore, it's better to use textContent not to cause unnecessary reflows. + return hiddenPre.textContent; +} /** * Escapes all potentially dangerous characters, so that the @@ -48645,24 +45881,24 @@ function encodeEntities(value) { /** * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.join('') to get out sanitized html string + * @param {Array} buf use buf.jain('') to get out sanitized html string * @returns {object} in the form of { - * start: function(tag, attrs) {}, + * start: function(tag, attrs, unary) {}, * end: function(tag) {}, * chars: function(text) {}, * comment: function(text) {} * } */ function htmlSanitizeWriter(buf, uriValidator) { - var ignoreCurrentElement = false; + var ignore = false; var out = angular.bind(buf, buf.push); return { - start: function(tag, attrs) { + start: function(tag, attrs, unary) { tag = angular.lowercase(tag); - if (!ignoreCurrentElement && blockedElements[tag]) { - ignoreCurrentElement = tag; + if (!ignore && specialElements[tag]) { + ignore = tag; } - if (!ignoreCurrentElement && validElements[tag] === true) { + if (!ignore && validElements[tag] === true) { out('<'); out(tag); angular.forEach(attrs, function(value, key) { @@ -48677,63 +45913,29 @@ function htmlSanitizeWriter(buf, uriValidator) { out('"'); } }); - out('>'); + out(unary ? '/>' : '>'); } }, end: function(tag) { - tag = angular.lowercase(tag); - if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { - out('</'); - out(tag); - out('>'); - } - if (tag == ignoreCurrentElement) { - ignoreCurrentElement = false; - } - }, + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out('</'); + out(tag); + out('>'); + } + if (tag == ignore) { + ignore = false; + } + }, chars: function(chars) { - if (!ignoreCurrentElement) { - out(encodeEntities(chars)); + if (!ignore) { + out(encodeEntities(chars)); + } } - } }; } -/** - * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare - * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want - * to allow any of these custom attributes. This method strips them all. - * - * @param node Root element to process - */ -function stripCustomNsAttrs(node) { - if (node.nodeType === 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.indexOf('ns1:') === 0) { - node.removeAttributeNode(attrNode); - i--; - l--; - } - } - } - - var nextNode = node.firstChild; - if (nextNode) { - stripCustomNsAttrs(nextNode); - } - - nextNode = node.nextSibling; - if (nextNode) { - stripCustomNsAttrs(nextNode); - } -} - - - // define ngSanitize module and register $sanitize service angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); @@ -48745,25 +45947,14 @@ 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/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 {object|function(url)} [attributes] Add custom attributes to the link element. - * - * Can be one of: - * - * - `object`: A map of attributes - * - `function`: Takes the url as a parameter and returns a map of attributes - * - * If the map of attributes contains a value for `target`, it overrides the value of - * the target parameter. - * - * - * @returns {string} Html-linkified and {@link $sanitize sanitized} text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. + * @returns {string} Html-linkified text. * * @usage <span ng-bind-html="linky_expression | linky"></span> @@ -48771,13 +45962,25 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); * @example <example module="linkyExample" deps="angular-sanitize.js"> <file name="index.html"> + <script> + 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'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithTarget = 'http://angularjs.org/'; + }]); + </script> <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> <table> <tr> - <th>Filter</th> - <th>Source</th> - <th>Rendered</th> + <td>Filter</td> + <td>Source</td> + <td>Rendered</td> </tr> <tr id="linky-filter"> <td>linky filter</td> @@ -48791,19 +45994,10 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); <tr id="linky-target"> <td>linky target</td> <td> - <pre><div ng-bind-html="snippetWithSingleURL | linky:'_blank'"><br></div></pre> - </td> - <td> - <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div> - </td> - </tr> - <tr id="linky-custom-attributes"> - <td>linky custom attributes</td> - <td> - <pre><div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"><br></div></pre> + <pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre> </td> <td> - <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div> + <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div> </td> </tr> <tr id="escaped-html"> @@ -48813,18 +46007,6 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); </tr> </table> </file> - <file name="script.js"> - 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'+ - 'and one more: ftp://127.0.0.1/.'; - $scope.snippetWithSingleURL = 'http://angularjs.org/'; - }]); - </file> <file name="protractor.js" type="protractor"> it('should linkify the snippet with urls', function() { expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). @@ -48852,32 +46034,20 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); it('should work with the target property', function() { expect(element(by.id('linky-target')). - element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). toBe('http://angularjs.org/'); expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); }); - - it('should optionally add custom attributes', function() { - expect(element(by.id('linky-custom-attributes')). - element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); - }); </file> </example> */ 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, + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/i, MAILTO_REGEXP = /^mailto:/i; - var linkyMinErr = angular.$$minErr('linky'); - var isString = angular.isString; - - return function(text, target, attributes) { - if (text == null || text === '') return text; - if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); - + return function(text, target) { + if (!text) return text; var match; var raw = text; var html = []; @@ -48906,19 +46076,8 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { } function addLink(url, text) { - var key; html.push('<a '); - if (angular.isFunction(attributes)) { - attributes = attributes(url); - } - if (angular.isObject(attributes)) { - for (key in attributes) { - html.push(key + '="' + attributes[key] + '" '); - } - } else { - attributes = {}; - } - if (angular.isDefined(target) && !('target' in attributes)) { + if (angular.isDefined(target)) { html.push('target="', target, '" '); @@ -53185,7 +50344,7 @@ angular.module('ui.router.state') * Copyright 2015 Drifty Co. * http://drifty.com/ * - * Ionic, v1.3.3 + * Ionic, v1.2.4-nightly-1917 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * @@ -54080,16 +51239,6 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ // it's back view would be better represented using the current view as its back view tmp = getViewById(switchToView.backViewId); if (tmp && switchToView.historyId !== tmp.historyId) { - // the new view is being removed from it's old position in the history and being placed at the top, - // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops - var viewIds = Object.keys(viewHistory.views); - viewIds.forEach(function(viewId) { - var view = viewHistory.views[viewId]; - if ((view.backViewId === switchToView.viewId) && (view.historyId !== switchToView.historyId)) { - view.backViewId = null; - } - }); - hist.stack[hist.cursor].backViewId = currentView.viewId; } @@ -54098,6 +51247,7 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ // create an element from the viewLocals template ele = $ionicViewSwitcher.createViewEle(viewLocals); if (this.isAbstractEle(ele, viewLocals)) { + void 0; return { action: 'abstractView', direction: DIRECTION_NONE, @@ -54137,7 +51287,6 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ direction = DIRECTION_FORWARD; } else if (currentView.historyId !== hist.historyId) { - // DB: this is a new view in a different tab direction = DIRECTION_ENTER; tmp = getHistoryById(currentView.historyId); @@ -54219,6 +51368,8 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ } } + void 0; + hist.cursor = viewHistory.currentView.index; return { @@ -54404,44 +51555,13 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ if (clearStateIds.length) { $timeout(function() { self.clearCache(clearStateIds); - }, 300); + }, 600); } } viewHistory.backView && viewHistory.backView.go(); }, - /** - * @ngdoc method - * @name $ionicHistory#removeBackView - * @description Remove the previous view from the history completely, including the - * cached element and scope (if they exist). - */ - removeBackView: function() { - var self = this; - var currentHistory = viewHistory.histories[this.currentHistoryId()]; - var currentCursor = currentHistory.cursor; - - var currentView = currentHistory.stack[currentCursor]; - var backView = currentHistory.stack[currentCursor - 1]; - var replacementView = currentHistory.stack[currentCursor - 2]; - - // fail if we dont have enough views in the history - if (!backView || !replacementView) { - return; - } - - // remove the old backView and the cached element/scope - currentHistory.stack.splice(currentCursor - 1, 1); - self.clearCache([backView.viewId]); - // make the replacementView and currentView point to each other (bypass the old backView) - currentView.backViewId = replacementView.viewId; - currentView.index = currentView.index - 1; - replacementView.forwardViewId = currentView.viewId; - // update the cursor and set new backView - viewHistory.backView = replacementView; - currentHistory.currentCursor += -1; - }, enabledBack: function(view) { var backView = getBackView(view); @@ -54608,10 +51728,6 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ if (ele && ele.attr('can-swipe-back') === 'false') { return false; } - var eleChild = ele.find('ion-view'); - if (eleChild && eleChild.attr('can-swipe-back') === 'false') { - return false; - } return true; } @@ -54721,7 +51837,7 @@ function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory * $ionicConfigProvider.views.maxCache(10); * ``` * - * Additionally, each platform can have its own config within the `$ionicConfigProvider.platform` + * Additionally, each platform can have it's own config within the `$ionicConfigProvider.platform` * property. The config below would only apply to Android devices. * * ```js @@ -54785,16 +51901,6 @@ function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory * @returns {boolean} */ - /** - * @ngdoc method - * @name $ionicConfigProvider#views.swipeBackEnabled - * @description By default on iOS devices, swipe to go back functionality is enabled by default. - * This method can be used to disable it globally, or on a per-view basis. - * Note: This functionality is only supported on iOS. - * @param {boolean} value - * @returns {boolean} - */ - /** * @ngdoc method * @name $ionicConfigProvider#scrolling.jsScrolling @@ -55194,28 +52300,27 @@ IonicModule provider.transitions.views.android = function(enteringEle, leavingEle, direction, shouldAnimate) { shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); - function setStyles(ele, x, opacity) { + function setStyles(ele, x) { var css = {}; css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; - css.opacity = opacity; ionic.DomUtil.cachedStyles(ele, css); } var d = { run: function(step) { if (direction == 'forward') { - setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker - setStyles(leavingEle, step * -100, 1); + setStyles(enteringEle, (1 - step) * 99); // starting at 98% prevents a flicker + setStyles(leavingEle, step * -100); } else if (direction == 'back') { - setStyles(enteringEle, (1 - step) * -100, 1); - setStyles(leavingEle, step * 100, 1); + setStyles(enteringEle, (1 - step) * -100); + setStyles(leavingEle, step * 100); } else { // swap, enter, exit - setStyles(enteringEle, 0, 1); - setStyles(leavingEle, 0, 0); + setStyles(enteringEle, 0); + setStyles(leavingEle, 0); } }, shouldAnimate: shouldAnimate @@ -55373,6 +52478,10 @@ var LOADING_TPL = '</div>' + '</div>'; +var LOADING_HIDE_DEPRECATED = '$ionicLoading instance.hide() has been deprecated. Use $ionicLoading.hide().'; +var LOADING_SHOW_DEPRECATED = '$ionicLoading instance.show() has been deprecated. Use $ionicLoading.show().'; +var LOADING_SET_DEPRECATED = '$ionicLoading instance.setContent() has been deprecated. Use $ionicLoading.show({ template: \'my content\' }).'; + /** * @ngdoc service * @name $ionicLoading @@ -55387,16 +52496,11 @@ var LOADING_TPL = * .controller('LoadingCtrl', function($scope, $ionicLoading) { * $scope.show = function() { * $ionicLoading.show({ - * template: 'Loading...', - * duration: 3000 - * }).then(function(){ - * console.log("The loading indicator is now displayed"); + * template: 'Loading...' * }); * }; * $scope.hide = function(){ - * $ionicLoading.hide().then(function(){ - * console.log("The loading indicator is now hidden"); - * }); + * $ionicLoading.hide(); * }; * }); * ``` @@ -55416,10 +52520,7 @@ var LOADING_TPL = * }); * app.controller('AppCtrl', function($scope, $ionicLoading) { * $scope.showLoading = function() { - * //options default to values in $ionicLoadingConfig - * $ionicLoading.show().then(function(){ - * console.log("The loading indicator is now displayed"); - * }); + * $ionicLoading.show(); //options default to values in $ionicLoadingConfig * }; * }); * ``` @@ -55454,8 +52555,9 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, * @ngdoc method * @name $ionicLoading#show * @description Shows a loading indicator. If the indicator is already shown, - * it will set the options given and keep the indicator shown. - * @returns {promise} A promise which is resolved when the loading indicator is presented. + * it will set the options given and keep the indicator shown. Note: While this + * function still returns an $ionicLoading instance for backwards compatiblity, + * its use has been deprecated. * @param {object} opts The options for the loading indicator. Available properties: * - `{string=}` `template` The html content of the indicator. * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator. @@ -55472,7 +52574,6 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, * @ngdoc method * @name $ionicLoading#hide * @description Hides the loading indicator, if shown. - * @returns {promise} A promise which is resolved when the loading indicator is hidden. */ hide: hideLoader, /** @@ -55570,8 +52671,6 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, function showLoader(options) { options = extend({}, $ionicLoadingConfig || {}, options || {}); - // use a default delay of 100 to avoid some issues reported on github - // https://github.com/driftyco/ionic/issues/3717 var delay = options.delay || options.showDelay || 0; deregisterStateListener1(); @@ -55584,17 +52683,34 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, //If loading.show() was called previously, cancel it and show with our new options $timeout.cancel(loadingShowDelay); loadingShowDelay = $timeout(noop, delay); - return loadingShowDelay.then(getLoader).then(function(loader) { + loadingShowDelay.then(getLoader).then(function(loader) { return loader.show(options); }); + + return { + hide: function deprecatedHide() { + $log.error(LOADING_HIDE_DEPRECATED); + return hideLoader.apply(this, arguments); + }, + show: function deprecatedShow() { + $log.error(LOADING_SHOW_DEPRECATED); + return showLoader.apply(this, arguments); + }, + setContent: function deprecatedSetContent(content) { + $log.error(LOADING_SET_DEPRECATED); + return getLoader().then(function(loader) { + loader.show({ template: content }); + }); + } + }; } function hideLoader() { deregisterStateListener1(); deregisterStateListener2(); $timeout.cancel(loadingShowDelay); - return getLoader().then(function(loader) { - return loader.hide(); + getLoader().then(function(loader) { + loader.hide(); }); } }]); @@ -55603,7 +52719,6 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, * @ngdoc service * @name $ionicModal * @module ionic - * @codepen gblny * @description * * Related: {@link ionic.controller:ionicModal ionicModal controller}. @@ -55649,7 +52764,7 @@ function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, * $scope.closeModal = function() { * $scope.modal.hide(); * }; - * // Cleanup the modal when we're done with it! + * //Cleanup the modal when we're done with it! * $scope.$on('$destroy', function() { * $scope.modal.remove(); * }); @@ -55783,7 +52898,6 @@ function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTempl self.el.classList.add('active'); self.scope.$broadcast('$ionicHeader.align'); self.scope.$broadcast('$ionicFooter.align'); - self.scope.$broadcast('$ionic.modalPresented'); }, 20); return $timeout(function() { @@ -55826,8 +52940,6 @@ function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTempl if (self._isShown) return; modalEl.addClass('ng-leave-active') .removeClass('ng-enter ng-enter-active active'); - - self.scope.$broadcast('$ionic.modalRemoved'); }, 20, false); self.$el.off('click'); @@ -55843,9 +52955,7 @@ function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTempl } return $timeout(function() { - if (!modalStack.length) { - $ionicBody.removeClass(self.viewType + '-open'); - } + $ionicBody.removeClass(self.viewType + '-open'); self.el.classList.add('hide'); }, self.hideDelay || 320); }, @@ -55857,24 +52967,10 @@ function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTempl * @returns {promise} A promise which is resolved when the modal is finished animating out. */ remove: function() { - var self = this, - deferred, promise; + var self = this; self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self); - // Only hide modal, when it is actually shown! - // The hide function shows a click-block-div for a split second, because on iOS, - // clicks will sometimes bleed through/ghost click on underlying elements. - // However, this will make the app unresponsive for short amount of time. - // We don't want that, if the modal window is already hidden. - if (self._isShown) { - promise = self.hide(); - } else { - deferred = $$q.defer(); - deferred.resolve(); - promise = deferred.promise; - } - - return promise.then(function() { + return self.hide().then(function() { self.scope.$destroy(); self.$el.remove(); }); @@ -56200,8 +53296,8 @@ IonicModule * @description * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`, * `offline`, etc. More information about available event types can be found in - * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). - * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). + * [Cordova's event documentation](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events). + * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/edge/cordova_events_events.md.html#Events). * @param {function} callback Called when the Cordova event is fired. * @returns {function} Returns a deregistration function to remove the event listener. */ @@ -56229,11 +53325,6 @@ IonicModule var q = $q.defer(); ionic.Platform.ready(function() { - - window.addEventListener('statusTap', function() { - $ionicScrollDelegate.scrollTop(true); - }); - q.resolve(); cb && cb(); }); @@ -56242,6 +53333,10 @@ IonicModule } }; + window.addEventListener('statusTap', function() { + $ionicScrollDelegate.scrollTop(true); + }); + return self; }] }; @@ -56312,7 +53407,7 @@ IonicModule * $scope.$on('$destroy', function() { * $scope.popover.remove(); * }); - * // Execute action on hidden popover + * // Execute action on hide popover * $scope.$on('popover.hidden', function() { * // Execute action * }); @@ -57189,7 +54284,6 @@ IonicModule * @returns {object} The scroll position of this view, with the following properties: * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). - * - `{number}` `zoom` The current zoom level. */ 'getScrollPosition', /** @@ -58067,67 +55161,32 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe }, - emit: function(step, enteringData, leavingData) { - var enteringScope = getScopeForElement(enteringEle, enteringData); - var leavingScope = getScopeForElement(leavingEle, leavingData); - - var prefixesAreEqual; + emit: function(step, enteringData, leavingData) { + var enteringScope = enteringEle.scope(), + leavingScope = leavingEle && leavingEle.scope(); - if ( !enteringData.viewId || enteringData.abstractView ) { - // it's an abstract view, so treat it accordingly + if (step == 'after') { + if (enteringScope) { + enteringScope.$emit('$ionicView.enter', enteringData); + } - // we only get access to the leaving scope once in the transition, - // so dispatch all events right away if it exists - if ( leavingScope ) { - leavingScope.$emit('$ionicView.beforeLeave', leavingData); + if (leavingScope) { leavingScope.$emit('$ionicView.leave', leavingData); - leavingScope.$emit('$ionicView.afterLeave', leavingData); - leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData); - leavingScope.$broadcast('$ionicParentView.leave', leavingData); - leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData); + + } else if (enteringScope && leavingData && leavingData.viewId) { + enteringScope.$emit('$ionicNavView.leave', leavingData); } } - else { - // it's a regular view, so do the normal process - if (step == 'after') { - if (enteringScope) { - enteringScope.$emit('$ionicView.enter', enteringData); - enteringScope.$broadcast('$ionicParentView.enter', enteringData); - } - if (leavingScope) { - leavingScope.$emit('$ionicView.leave', leavingData); - leavingScope.$broadcast('$ionicParentView.leave', leavingData); - } - else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { - // we only want to dispatch this when we are doing a single-tier - // state change such as changing a tab, so compare the state - // for the same state-prefix but different suffix - prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); - if ( prefixesAreEqual ) { - enteringScope.$emit('$ionicNavView.leave', leavingData); - } - } - } + if (enteringScope) { + enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); + } - if (enteringScope) { - enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); - enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData); - } + if (leavingScope) { + leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); - if (leavingScope) { - leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); - leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData); - - } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { - // we only want to dispatch this when we are doing a single-tier - // state change such as changing a tab, so compare the state - // for the same state-prefix but different suffix - prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); - if ( prefixesAreEqual ) { - enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); - } - } + } else if (enteringScope && leavingData && leavingData.viewId) { + enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); } }, @@ -58211,15 +55270,6 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe containerEle.innerHTML = viewLocals.$template; if (containerEle.children.length === 1) { containerEle.children[0].classList.add('pane'); - if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) { - angular.element(containerEle.children[0]).attr("abstract", "true"); - } - else { - if ( viewLocals.$$state && viewLocals.$$state.self ) { - angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name); - } - - } return jqLite(containerEle.children[0]); } } @@ -58304,69 +55354,6 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe } } - function compareStatePrefixes(enteringStateName, exitingStateName) { - var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.'); - var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.'); - - // if either of the prefixes are empty, just return false - if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) { - return false; - } - - var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex); - var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex); - - return enteringPrefix === exitingPrefix; - } - - function getScopeForElement(element, stateData) { - if ( !element ) { - return null; - } - // check if it's abstract - var attributeValue = angular.element(element).attr("abstract"); - var stateValue = angular.element(element).attr("state"); - - if ( attributeValue !== "true" ) { - // it's not an abstract view, so make sure the element - // matches the state. Due to abstract view weirdness, - // sometimes it doesn't. If it doesn't, don't dispatch events - // so leave the scope undefined - if ( stateValue === stateData.stateName ) { - return angular.element(element).scope(); - } - return null; - } - else { - // it is an abstract element, so look for element with the "state" attributeValue - // set to the name of the stateData state - var elements = aggregateNavViewChildren(element); - for ( var i = 0; i < elements.length; i++ ) { - var state = angular.element(elements[i]).attr("state"); - if ( state === stateData.stateName ) { - stateData.abstractView = true; - return angular.element(elements[i]).scope(); - } - } - // we didn't find a match, so return null - return null; - } - } - - function aggregateNavViewChildren(element) { - var aggregate = []; - var navViews = angular.element(element).find("ion-nav-view"); - for ( var i = 0; i < navViews.length; i++ ) { - var children = angular.element(navViews[i]).children(); - var childrenAggregated = []; - for ( var j = 0; j < children.length; j++ ) { - childrenAggregated = childrenAggregated.concat(children[j]); - } - aggregate = aggregate.concat(childrenAggregated); - } - return aggregate; - } - }]); /** @@ -58597,20 +55584,10 @@ function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { self.titleTextWidth = function() { - var element = getEle(TITLE); - if ( element ) { - // If the element has a nav-bar-title, use that instead - // to calculate the width of the title - var children = angular.element(element).children(); - for ( var i = 0; i < children.length; i++ ) { - if ( angular.element(children[i]).hasClass('nav-bar-title') ) { - element = children[i]; - break; - } - } + if (!titleTextWidth) { + var bounds = ionic.DomUtil.getTextBounds(getEle(TITLE)); + titleTextWidth = Math.min(bounds && bounds.width || 30); } - var bounds = ionic.DomUtil.getTextBounds(element); - titleTextWidth = Math.min(bounds && bounds.width || 30); return titleTextWidth; }; @@ -59648,8 +56625,7 @@ IonicModule '$ionicViewSwitcher', '$ionicConfig', '$ionicScrollDelegate', - '$ionicSideMenuDelegate', -function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate, $ionicSideMenuDelegate) { +function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate) { var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_DESTROY_ELE = '$destroyEle'; @@ -59700,8 +56676,8 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $scope.$on('$ionicTabs.leave', onTabsLeave); ionic.Platform.ready(function() { - if ( ionic.Platform.isWebView() && ionic.Platform.isIOS() ) { - self.initSwipeBack(); + if (ionic.Platform.isWebView() && $ionicConfig.views.swipeBackEnabled()) { + self.initSwipeBack(); } }); @@ -59843,7 +56819,6 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) { viewScope = viewElement.scope(); viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data); - viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data); break; } } @@ -59992,8 +56967,7 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, var cancelData = {}; function onDragStart(ev) { - if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight() ) return; - + if (!isPrimary) return; startDragX = getDragX(ev); if (startDragX > swipeBackHitWidth) return; @@ -60319,7 +57293,7 @@ IonicModule } function overscroll(val) { - scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)'; + scrollChild.style[ionic.CSS.TRANSFORM] = 'translateY(' + val + 'px)'; lastOverscroll = val; } @@ -60455,17 +57429,13 @@ IonicModule }; function destroy() { - if ( scrollChild ) { - ionic.off(touchStartEvent, handleTouchstart, scrollChild); - ionic.off(touchMoveEvent, handleTouchmove, scrollChild); - ionic.off(touchEndEvent, handleTouchend, scrollChild); - ionic.off('mousedown', handleMousedown, scrollChild); - ionic.off('mousemove', handleTouchmove, scrollChild); - ionic.off('mouseup', handleTouchend, scrollChild); - } - if ( scrollParent ) { - ionic.off('scroll', handleScroll, scrollParent); - } + ionic.off(touchStartEvent, handleTouchstart, scrollChild); + ionic.off(touchMoveEvent, handleTouchmove, scrollChild); + ionic.off(touchEndEvent, handleTouchend, scrollChild); + ionic.off('mousedown', handleMousedown, scrollChild); + ionic.off('mousemove', handleTouchmove, scrollChild); + ionic.off('mouseup', handleTouchend, scrollChild); + ionic.off('scroll', handleScroll, scrollParent); scrollParent = null; scrollChild = null; } @@ -60617,17 +57587,8 @@ function($scope, deregisterInstance(); scrollView && scrollView.__cleanup && scrollView.__cleanup(); angular.element($window).off('resize', resize); - if ( $element ) { - $element.off('scroll', scrollFunc); - } - if ( self._scrollViewOptions ) { - self._scrollViewOptions.el = null; - } - if ( scrollViewOptions ) { - scrollViewOptions.el = null; - } - - scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null; + $element.off('scroll', scrollFunc); + scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = scrollViewOptions.el = self._scrollViewOptions.el = $element = self.$element = element = null; }); $timeout(function() { @@ -60955,15 +57916,20 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $io self.content.setTranslateX(amount); - leftShowing = amount > 0; - rightShowing = amount < 0; + if (amount >= 0) { + leftShowing = true; + rightShowing = false; - if (amount > 0) { - // Push the z-index of the right menu down - self.right && self.right.pushDown && self.right.pushDown(); - // Bring the z-index of the left menu up - self.left && self.left.bringUp && self.left.bringUp(); + if (amount > 0) { + // Push the z-index of the right menu down + self.right && self.right.pushDown && self.right.pushDown(); + // Bring the z-index of the left menu up + self.left && self.left.bringUp && self.left.bringUp(); + } } else { + rightShowing = true; + leftShowing = false; + // Bring the z-index of the right menu up self.right && self.right.bringUp && self.right.bringUp(); // Push the z-index of the left menu down @@ -61532,8 +58498,7 @@ function($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $io var animations = { android: function(ele) { - // Note that this is called as a function, not a constructor. - var self = {}; + var self = this; this.stop = false; @@ -61769,8 +58734,6 @@ function($scope, $element, $ionicHistory) { uiSref: tab.uiSref }); } - - $scope.$broadcast("tabSelected", { selectedTab: tab, selectedTabIndex: tabIndex}); } }; @@ -63254,17 +60217,13 @@ IonicModule.directive('exposeAsideWhen', ['$window', function($window) { require: '^ionSideMenus', link: function($scope, $element, $attr, sideMenuCtrl) { - var prevInnerWidth = $window.innerWidth; - var prevInnerHeight = $window.innerHeight; - - ionic.on('resize', function() { - if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) { - return; - } - prevInnerWidth = $window.innerWidth; - prevInnerHeight = $window.innerHeight; + // Setup a match media query listener that triggers a ui change only when a change + // in media matching status occurs + var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen; + var mql = $window.matchMedia(mq); + mql.addListener(function() { onResize(); - }, $window); + }); function checkAsideExpose() { var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen; @@ -63626,7 +60585,7 @@ IonicModule * <button class="button">Right Button</button> * </div> * </ion-header-bar> - * <ion-content class="has-header"> + * <ion-content> * Some content! * </ion-content> * ``` @@ -63653,7 +60612,7 @@ IonicModule * * @usage * ```html - * <ion-content class="has-footer"> + * <ion-content> * Some content! * </ion-content> * <ion-footer-bar align-title="left" class="bar-assertive"> @@ -63791,7 +60750,7 @@ function headerFooterBarDirective(isHeader) { * reach to trigger the on-infinite expression. Default: 1%. * @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons. - * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'. This is depreciated + * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'. This is depreicated * in favor of the SVG {@link ionic.directive:ionSpinner}. * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load. * @@ -63893,7 +60852,7 @@ IonicModule * <ion-list> * <ion-input> * <input type="text" placeholder="First Name"> -* </ion-input> +* <ion-input> * * <ion-input> * <ion-label>Username</ion-label> @@ -64249,9 +61208,9 @@ IonicModule.directive('ionOptionButton', [function() { return function($scope, $element, $attr, itemCtrl) { if (!itemCtrl.optionsContainer) { itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS); - itemCtrl.$element.prepend(itemCtrl.optionsContainer); + itemCtrl.$element.append(itemCtrl.optionsContainer); } - itemCtrl.optionsContainer.prepend($element); + itemCtrl.optionsContainer.append($element); itemCtrl.$element.addClass('item-right-editable'); @@ -64961,6 +61920,7 @@ IonicModule * @param {boolean=} no-tap-scroll By default, the navbar will scroll the content * to the top when tapped. Set no-tap-scroll to true to disable this behavior. * + * </table><br/> */ IonicModule .directive('ionNavBar', function() { @@ -65335,9 +62295,6 @@ IonicModule * <iframe width="560" height="315" src="//www.youtube.com/embed/dqJRoh8MnBo" * frameborder="0" allowfullscreen></iframe> * - * Note: We do not recommend using [resolve](https://github.com/angular-ui/ui-router/wiki#resolve) - * of AngularUI Router. The recommended approach is to execute any logic needed before beginning the state transition. - * * @param {string=} name A view name. The name should be unique amongst the other views in the * same state. You can have views of the same name that live in different states. For more * information, see ui-router's @@ -65634,7 +62591,7 @@ IonicModule * is now the default, replacing rotating font icons. Set to `none` to disable both the * spinner and the icon. * @param {string=} refreshing-icon The font icon to display after user lets go of the - * refresher. This is deprecated in favor of the SVG {@link ionic.directive:ionSpinner}. + * refresher. This is depreicated in favor of the SVG {@link ionic.directive:ionSpinner}. * @param {boolean=} disable-pulling-rotation Disables the rotation animation of the pulling * icon when it reaches its activated threshold. To be used with a custom `pulling-icon`. * @@ -65679,9 +62636,7 @@ IonicModule $scope.$on('scroll.refreshComplete', function() { $scope.$evalAsync(function() { - if(scrollCtrl.scrollView){ - scrollCtrl.scrollView.finishPullToRefresh(); - } + scrollCtrl.scrollView.finishPullToRefresh(); }); }); } @@ -65723,7 +62678,6 @@ IonicModule * @param {boolean=} paging Whether to scroll with paging. * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}. * @param {expression=} on-scroll Called whenever the user scrolls. - * @param {expression=} on-scroll-complete Called whenever the scrolling paging is completed. * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true. * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true. * @param {boolean=} zooming Whether to support pinch-to-zoom @@ -65759,7 +62713,6 @@ function($timeout, $controller, $ionicBind, $ionicConfig) { direction: '@', paging: '@', $onScroll: '&onScroll', - $onScrollComplete: '&onScrollComplete', scroll: '@', scrollbarX: '@', scrollbarY: '@', @@ -65801,8 +62754,7 @@ function($timeout, $controller, $ionicBind, $ionicConfig) { maxZoom: $scope.$eval($scope.maxZoom) || 3, minZoom: $scope.$eval($scope.minZoom) || 0.5, preventDefault: true, - nativeScrolling: nativeScrolling, - scrollingComplete: onScrollComplete + nativeScrolling: nativeScrolling }; if (isPaging) { @@ -65810,17 +62762,10 @@ function($timeout, $controller, $ionicBind, $ionicConfig) { scrollViewOptions.bouncing = false; } - var scrollCtrl = $controller('$ionicScroll', { + $controller('$ionicScroll', { $scope: $scope, scrollViewOptions: scrollViewOptions }); - - function onScrollComplete() { - $scope.$onScrollComplete && $scope.$onScrollComplete({ - scrollTop: scrollCtrl.scrollView.__scrollTop, - scrollLeft: scrollCtrl.scrollView.__scrollLeft - }); - } } } }; @@ -66246,7 +63191,6 @@ IonicModule * @ngdoc directive * @name ionSlideBox * @module ionic - * @codepen AjgEB * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component. * Don't depend on the internal behavior of this widget. * @delegate ionic.service:$ionicSlideBoxDelegate @@ -66254,6 +63198,7 @@ IonicModule * @description * The Slide Box is a multi-page container where each page can be swiped or dragged between: * + *  * * @usage * ```html @@ -66402,7 +63347,7 @@ function($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $i link: function($scope, $element, $attr) { // Disable ngAnimate for slidebox and its children - $animate.enabled($element, false); + $animate.enabled(false, $element); // if showPager is undefined, show the pager if (!isDefined($attr.showPager)) { @@ -66479,6 +63424,7 @@ function($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $i * @ngdoc directive * @name ionSlides * @module ionic + * @delegate ionic.service:$ionicSlideBoxDelegate * @restrict E * @description * The Slides component is a powerful multi-page container where each page can be swiped or dragged between. @@ -66490,84 +63436,22 @@ function($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $i * * @usage * ```html - * <ion-content scroll="false"> - * <ion-slides options="options" slider="data.slider"> - * <ion-slide-page> - * <div class="box blue"><h1>BLUE</h1></div> - * </ion-slide-page> - * <ion-slide-page> - * <div class="box yellow"><h1>YELLOW</h1></div> - * </ion-slide-page> - * <ion-slide-page> - * <div class="box pink"><h1>PINK</h1></div> - * </ion-slide-page> - * </ion-slides> - * </ion-content> - * ``` - * - * ```js - * $scope.options = { - * loop: false, - * effect: 'fade', - * speed: 500, - * } - * - * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){ - * // data.slider is the instance of Swiper - * $scope.slider = data.slider; - * }); - * - * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){ - * console.log('Slide change is beginning'); - * }); - * - * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){ - * // note: the indexes are 0-based - * $scope.activeIndex = data.slider.activeIndex; - * $scope.previousIndex = data.slider.previousIndex; - * }); - * - * ``` - * - * ## Slide Events - * - * The slides component dispatches events when the active slide changes - * - * <table class="table"> - * <tr> - * <td><code>$ionicSlides.slideChangeStart</code></td> - * <td>This event is emitted when a slide change begins</td> - * </tr> - * <tr> - * <td><code>$ionicSlides.slideChangeEnd</code></td> - * <td>This event is emitted when a slide change completes</td> - * </tr> - * <tr> - * <td><code>$ionicSlides.sliderInitialized</code></td> - * <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td> - * </tr> - * </table> - * - * - * ## Updating Slides Dynamically - * When applying data to the slider at runtime, typically everything will work as expected. - * - * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly. - * - * ``` - * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){ - * // grab an instance of the slider - * $scope.slider = data.slider; - * }); - * - * function dataChangeHandler(){ - * // call this function when data changes, such as an HTTP request, etc - * if ( $scope.slider ){ - * $scope.slider.updateLoop(); - * } - * } + * <ion-slides on-slide-changed="slideHasChanged($index)"> + * <ion-slide-page> + * <div class="box blue"><h1>BLUE</h1></div> + * </ion-slide-page> + * <ion-slide-page> + * <div class="box yellow"><h1>YELLOW</h1></div> + * </ion-slide-page> + * <ion-slide-page> + * <div class="box pink"><h1>PINK</h1></div> + * </ion-slide-page> + * </ion-slides> * ``` * + * @param {string=} delegate-handle The handle used to identify this slideBox + * with {@link ionic.service:$ionicSlideBoxDelegate}. + * @param {object=} options to pass to the widget. See the full ist here: [http://www.idangero.us/swiper/api/](http://www.idangero.us/swiper/api/) */ IonicModule .directive('ionSlides', [ @@ -66601,17 +63485,10 @@ function($animate, $timeout, $compile) { _this.__slider.createLoop(); } - var slidesLength = _this.__slider.slides.length; - // Don't allow pager to show with > 10 slides - if (slidesLength > 10) { + if (_this.__slider.slides.length > 10) { $scope.showPager = false; } - - // When slide index is greater than total then slide to last index - if (_this.__slider.activeIndex > slidesLength - 1) { - _this.__slider.slideTo(slidesLength - 1); - } }); }; @@ -66626,7 +63503,7 @@ function($animate, $timeout, $compile) { var options = $scope.options || {}; var newOptions = angular.extend({ - pagination: $element.children().children()[1], + pagination: '.swiper-pagination', paginationClickable: true, lazyLoading: true, preloadImages: false @@ -66637,24 +63514,17 @@ function($animate, $timeout, $compile) { $timeout(function() { var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile); - $scope.$emit("$ionicSlides.sliderInitialized", { slider: slider }); - _this.__slider = slider; $scope.slider = _this.__slider; $scope.$on('$destroy', function() { slider.destroy(); - _this.__slider = null; }); }); - $timeout(function() { - // if it's a loop, render the slides again just incase - _this.rapidUpdate(); - }, 200); - }], + link: function($scope) { $scope.showPager = true; // Disable ngAnimate for slidebox and its children @@ -66671,10 +63541,6 @@ function($animate, $timeout, $compile) { template: '<div class="swiper-slide" ng-transclude></div>', link: function($scope, $element, $attr, ionSlidesCtrl) { ionSlidesCtrl.rapidUpdate(); - - $scope.$on('$destroy', function() { - ionSlidesCtrl.rapidUpdate(); - }); } }; }]); @@ -67089,10 +63955,11 @@ IonicModule replace: true, require: ['^ionTabs', '^ionTab'], template: - '<a ng-class="{\'has-badge\':badge, \'tab-hidden\':isHidden(), \'tab-item-active\': isTabActive()}" ' + + '<a ng-class="{\'tab-item-active\': isTabActive(), \'has-badge\':badge, \'tab-hidden\':isHidden()}" ' + ' ng-disabled="disabled()" class="tab-item">' + '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' + - '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' + + '<i class="icon {{getIconOn()}}" ng-if="getIconOn() && isTabActive()"></i>' + + '<i class="icon {{getIconOff()}}" ng-if="getIconOff() && !isTabActive()"></i>' + '<span class="tab-title" ng-bind-html="title"></span>' + '</a>', scope: { @@ -67140,17 +64007,6 @@ IonicModule $scope.isTabActive = function() { return tabsCtrl.selectedTab() === tabCtrl.$scope; }; - - $scope.getIcon = function() { - if ( tabsCtrl.selectedTab() === tabCtrl.$scope ) { - // active - return $scope.iconOn || $scope.icon; - } - else { - // inactive - return $scope.iconOff || $scope.icon; - } - }; } }; }]); @@ -67449,10 +64305,6 @@ function($timeout, $ionicConfig) { * show. Also contained is transition data, such as the transition type and * direction that will be or was used. * - * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is - * desirable for a child/nested view to be notified of the event. - * For this use case, `$ionicParentView` life cycle events are broadcast downwards. - * * <table class="table"> * <tr> * <td><code>$ionicView.loaded</code></td> @@ -67493,56 +64345,8 @@ function($timeout, $ionicConfig) { * <td>The view's controller has been destroyed and its element has been * removed from the DOM.</td> * </tr> - * <tr> - * <td><code>$ionicParentView.enter</code></td> - * <td>The parent view has fully entered and is now the active view. - * This event will fire, whether it was the first load or a cached view.</td> - * </tr> - * <tr> - * <td><code>$ionicParentView.leave</code></td> - * <td>The parent view has finished leaving and is no longer the - * active view. This event will fire, whether it is cached or destroyed.</td> - * </tr> - * <tr> - * <td><code>$ionicParentView.beforeEnter</code></td> - * <td>The parent view is about to enter and become the active view.</td> - * </tr> - * <tr> - * <td><code>$ionicParentView.beforeLeave</code></td> - * <td>The parent view is about to leave and no longer be the active view.</td> - * </tr> - * <tr> - * <td><code>$ionicParentView.afterEnter</code></td> - * <td>The parent view has fully entered and is now the active view.</td> - * </tr> - * <tr> - * <td><code>$ionicParentView.afterLeave</code></td> - * <td>The parent view has finished leaving and is no longer the active view.</td> - * </tr> * </table> * - * ## LifeCycle Event Usage - * - * Below is an example of how to listen to life cycle events and - * access state parameter data - * - * ```js - * $scope.$on("$ionicView.beforeEnter", function(event, data){ - * // handle event - * console.log("State Params: ", data.stateParams); - * }); - * - * $scope.$on("$ionicView.enter", function(event, data){ - * // handle event - * console.log("State Params: ", data.stateParams); - * }); - * - * $scope.$on("$ionicView.afterEnter", function(event, data){ - * // handle event - * console.log("State Params: ", data.stateParams); - * }); - * ``` - * * ## Caching * * Caching can be disabled and enabled in multiple ways. By default, Ionic will |
