diff --git a/src/app/directives/kibanaPanel.js b/src/app/directives/kibanaPanel.js index 377a0edd912..f9eeff54f1e 100755 --- a/src/app/directives/kibanaPanel.js +++ b/src/app/directives/kibanaPanel.js @@ -47,7 +47,7 @@ function (angular) { '' + ''+ - '' + + '' + ''+ '' + diff --git a/src/app/panels/filtering/module.html b/src/app/panels/filtering/module.html index b638a7ac17e..7d9b0669a35 100755 --- a/src/app/panels/filtering/module.html +++ b/src/app/panels/filtering/module.html @@ -61,7 +61,7 @@
diff --git a/src/app/panels/filtering/module.js b/src/app/panels/filtering/module.js index 78b0066d587..fe00abf4ecf 100755 --- a/src/app/panels/filtering/module.js +++ b/src/app/panels/filtering/module.js @@ -33,16 +33,16 @@ function (angular, app, _) { $scope.remove = function(id) { filterSrv.remove(id); - dashboard.refresh(); }; + // This function should be moved to the service $scope.toggle = function(id) { filterSrv.list[id].active = !filterSrv.list[id].active; dashboard.refresh(); }; $scope.refresh = function() { - $rootScope.$broadcast('refresh'); + dashboard.refresh(); }; $scope.render = function() { diff --git a/src/app/panels/histogram/module.js b/src/app/panels/histogram/module.js index 2d034e1d181..1731cb22318 100755 --- a/src/app/panels/histogram/module.js +++ b/src/app/panels/histogram/module.js @@ -312,9 +312,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) { to:moment.utc(_to), field:$scope.panel.time_field }); - - dashboard.refresh(); - }; // I really don't like this function, too much dom manip. Break out into directive? @@ -449,7 +446,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) { scope.plot = $.plot(elem, scope.data, options); } catch(e) { - console.log(e); + // Nothing to do here } } @@ -502,7 +499,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) { to : moment.utc(ranges.xaxis.to), field : scope.panel.time_field }); - dashboard.refresh(); }); } }; diff --git a/src/app/panels/map/module.js b/src/app/panels/map/module.js index 8d1d23739ff..02f5268281f 100755 --- a/src/app/panels/map/module.js +++ b/src/app/panels/map/module.js @@ -121,7 +121,6 @@ function (angular, app, _, $) { $scope.build_search = function(field,value) { filterSrv.set({type:'querystring',mandate:'must',query:field+":"+value}); - dashboard.refresh(); }; }); diff --git a/src/app/panels/pie/module.js b/src/app/panels/pie/module.js index 2232f737002..46feff92c97 100755 --- a/src/app/panels/pie/module.js +++ b/src/app/panels/pie/module.js @@ -175,7 +175,7 @@ define([ }); - module.directive('pie', function(querySrv, filterSrv, dashboard) { + module.directive('pie', function(querySrv, filterSrv) { return { restrict: 'A', link: function(scope, elem) { @@ -270,7 +270,6 @@ define([ } if(scope.panel.mode === 'terms') { filterSrv.set({type:'terms',field:scope.panel.query.field,value:object.series.label}); - dashboard.refresh(); } }); diff --git a/src/app/panels/table/micropanel.html b/src/app/panels/table/micropanel.html index 3abed9b26aa..b3d08fb2053 100755 --- a/src/app/panels/table/micropanel.html +++ b/src/app/panels/table/micropanel.html @@ -41,4 +41,4 @@
-{{field}} ({{Math.round((count / micropanel.count) * 100)}}%), \ No newline at end of file +{{field}} ({{Math.round((count / micropanel.count) * 100)}}%), \ No newline at end of file diff --git a/src/app/panels/table/module.js b/src/app/panels/table/module.js index bdab8919d11..a6c2f70ff17 100755 --- a/src/app/panels/table/module.js +++ b/src/app/panels/table/module.js @@ -160,14 +160,12 @@ function (angular, app, _, kbn, moment) { } else { query = angular.toJson(value); } - filterSrv.set({type:'field',field:field,query:query,mandate:(negate ? 'mustNot':'must')}); $scope.panel.offset = 0; - dashboard.refresh(); + filterSrv.set({type:'field',field:field,query:query,mandate:(negate ? 'mustNot':'must')}); }; $scope.fieldExists = function(field,mandate) { filterSrv.set({type:'exists',field:field,mandate:mandate}); - dashboard.refresh(); }; $scope.get_data = function(segment,query_id) { diff --git a/src/app/panels/terms/module.js b/src/app/panels/terms/module.js index d7d08c6b063..f1562f142b7 100755 --- a/src/app/panels/terms/module.js +++ b/src/app/panels/terms/module.js @@ -143,7 +143,6 @@ function (angular, app, _, $, kbn) { } else { return; } - dashboard.refresh(); }; $scope.set_refresh = function (state) { diff --git a/src/app/panels/timepicker/module.js b/src/app/panels/timepicker/module.js index 2403a1f6409..000021fc138 100755 --- a/src/app/panels/timepicker/module.js +++ b/src/app/panels/timepicker/module.js @@ -87,9 +87,6 @@ function (angular, app, _, moment, kbn) { set_time_filter($scope.time); } - dashboard.refresh(); - - // Start refresh timer if enabled if ($scope.panel.refresh.enable) { $scope.set_interval($scope.panel.refresh.interval); @@ -219,8 +216,7 @@ function (angular, app, _, moment, kbn) { // Update internal time object // Remove all other time filters - filterSrv.removeByType('time'); - + filterSrv.removeByType('time',true); $scope.time = $scope.time_calc(); $scope.time.field = $scope.panel.timefield; @@ -228,15 +224,14 @@ function (angular, app, _, moment, kbn) { update_panel(); set_time_filter($scope.time); - dashboard.refresh(); - }; - $scope.$watch('panel.mode', $scope.time_apply); + + //$scope.$watch('panel.mode', $scope.time_apply); function set_time_filter(time) { time.type = 'time'; // Clear all time filters, set a new one - filterSrv.removeByType('time'); + filterSrv.removeByType('time',true); $scope.panel.filter_id = filterSrv.set(compile_time(time)); return $scope.panel.filter_id; } diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html index 45c3d5441f5..1619600e345 100755 --- a/src/app/partials/dashboard.html +++ b/src/app/partials/dashboard.html @@ -35,7 +35,7 @@
-
+
@@ -52,7 +52,7 @@
-
+
diff --git a/src/app/services/filterSrv.js b/src/app/services/filterSrv.js index 3742a45aa1d..4f49f29f849 100755 --- a/src/app/services/filterSrv.js +++ b/src/app/services/filterSrv.js @@ -7,7 +7,7 @@ define([ var module = angular.module('kibana.services'); - module.service('filterSrv', function(dashboard, ejsResource) { + module.service('filterSrv', function(dashboard, ejsResource, $rootScope, $timeout) { // Create an object to hold our service state on the dashboard dashboard.current.services.filter = dashboard.current.services.filter || {}; @@ -44,19 +44,20 @@ define([ // This is used both for adding filters and modifying them. // If an id is passed, the filter at that id is updated - this.set = function(filter,id) { + this.set = function(filter,id,noRefresh) { + var _r; _.defaults(filter,{mandate:'must'}); filter.active = true; if(!_.isUndefined(id)) { if(!_.isUndefined(self.list[id])) { _.extend(self.list[id],filter); - return id; + _r = id; } else { - return false; + _r = false; } } else { if(_.isUndefined(filter.type)) { - return false; + _r = false; } else { var _id = nextId(); var _filter = { @@ -66,9 +67,54 @@ define([ _.defaults(filter,_filter); self.list[_id] = filter; self.ids.push(_id); - return _id; + _r = _id; } } + if(!$rootScope.$$phase) { + $rootScope.$apply(); + } + if(noRefresh !== true) { + $timeout(function(){ + dashboard.refresh(); + },0); + } + return _r; + }; + + this.remove = function(id,noRefresh) { + var _r; + if(!_.isUndefined(self.list[id])) { + delete self.list[id]; + // This must happen on the full path also since _.without returns a copy + self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id); + _f.idQueue.unshift(id); + _f.idQueue.sort(function(v,k){return v-k;}); + _r = true; + } else { + _r = false; + } + if(!$rootScope.$$phase) { + $rootScope.$apply(); + } + if(noRefresh !== true) { + $timeout(function(){ + dashboard.refresh(); + },0); + } + return _r; + }; + + this.removeByType = function(type,noRefresh) { + var ids = self.idsByType(type); + _.each(ids,function(id) { + self.remove(id,true); + }); + if(noRefresh !== true) { + $timeout(function(){ + dashboard.refresh(); + },0); + } + return ids; }; this.getBoolFilter = function(ids) { @@ -106,6 +152,7 @@ define([ case 'time': return ejs.RangeFilter(filter.field) .from(filter.from.valueOf()) + //.from("now-1d") .to(filter.to.valueOf()); case 'range': return ejs.RangeFilter(filter.field) @@ -130,14 +177,6 @@ define([ return _.pick(self.list,self.idsByType(type,inactive)); }; - this.removeByType = function(type) { - var ids = self.idsByType(type); - _.each(ids,function(id) { - self.remove(id); - }); - return ids; - }; - this.idsByType = function(type,inactive) { var _require = inactive ? {type:type} : {type:type,active:true}; return _.pluck(_.where(self.list,_require),'id'); @@ -171,20 +210,6 @@ define([ } }; - this.remove = function(id) { - if(!_.isUndefined(self.list[id])) { - delete self.list[id]; - // This must happen on the full path also since _.without returns a copy - self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id); - _f.idQueue.unshift(id); - _f.idQueue.sort(function(v,k){return v-k;}); - return true; - } else { - return false; - } - }; - - var nextId = function() { if(_f.idQueue.length > 0) { return _f.idQueue.shift(); diff --git a/src/vendor/angular/angular-sanitize.js b/src/vendor/angular/angular-sanitize.js index e017f6d4a90..a2d8d34cbdc 100755 --- a/src/vendor/angular/angular-sanitize.js +++ b/src/vendor/angular/angular-sanitize.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.0.8 + * @license AngularJS v1.1.5 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -10,25 +10,6 @@ * @ngdoc overview * @name ngSanitize * @description - * - * The `ngSanitize` module provides functionality to sanitize HTML. - * - * # Installation - * As a separate module, it must be loaded after Angular core is loaded; otherwise, an 'Uncaught Error: - * No module: ngSanitize' runtime error will occur. - * - *
- *   
- *   
- * 
- * - * # Usage - * To make sure the module is available to your application, declare it as a dependency of you application - * module. - * - *
- *   angular.module('app', ['ngSanitize']);
- * 
*/ /* @@ -148,7 +129,7 @@ var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?: BEGING_END_TAGE_REGEXP = /^<\s*\//, COMMENT_REGEXP = //g, CDATA_REGEXP = //g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/i, + URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/, NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) @@ -302,10 +283,10 @@ function htmlParser( html, handler ) { var attrs = {}; - rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue + || singleQoutedValue + || unqoutedValue || ''; attrs[name] = decodeEntities(value); @@ -452,6 +433,7 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san * plain email address links. * * @param {string} text Input text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. * @returns {string} Html-linkified text. * * @usage @@ -468,6 +450,7 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san 'mailto:us@somewhere.org,\n'+ 'another@somewhere.org,\n'+ 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithTarget = 'http://angularjs.org/'; }
@@ -487,6 +470,15 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san
+ + linky target + +
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
+ + +
+ + no filter
<div ng-bind="snippet">
</div>
@@ -519,6 +511,11 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san toBe('new http://link.'); expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); }); + + it('should work with the target property', function() { + expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")). + toBe('http://angularjs.org/'); + }); */ @@ -526,7 +523,7 @@ angular.module('ngSanitize').filter('linky', function() { var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, MAILTO_REGEXP = /^mailto:/; - return function(text) { + return function(text, target) { if (!text) return text; var match; var raw = text; @@ -535,6 +532,10 @@ angular.module('ngSanitize').filter('linky', function() { var writer = htmlSanitizeWriter(html); var url; var i; + var properties = {}; + if (angular.isDefined(target)) { + properties.target = target; + } while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; @@ -542,7 +543,8 @@ angular.module('ngSanitize').filter('linky', function() { if (match[2] == match[3]) url = 'mailto:' + url; i = match.index; writer.chars(raw.substr(0, i)); - writer.start('a', {href:url}); + properties.href = url; + writer.start('a', properties); writer.chars(match[0].replace(MAILTO_REGEXP, '')); writer.end('a'); raw = raw.substring(i + match[0].length); diff --git a/src/vendor/angular/angular.js b/src/vendor/angular/angular.js index d50515716ed..d2115fe1d80 100755 --- a/src/vendor/angular/angular.js +++ b/src/vendor/angular/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.0.8 + * @license AngularJS v1.1.5 * (c) 2010-2012 Google, Inc. http://angularjs.org * License: MIT */ @@ -61,12 +61,31 @@ var /** holds major version number for IE or NaN for real browsers */ push = [].push, toString = Object.prototype.toString, + + _angular = window.angular, /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, nodeName_, uid = ['0', '0', '0']; +/** + * @ngdoc function + * @name angular.noConflict + * @function + * + * @description + * Restores the previous global value of angular and returns the current instance. Other libraries may already use the + * angular namespace. Or a previous version of angular is already loaded on the page. In these cases you may want to + * restore the previous namespace and keep a reference to angular. + * + * @return {Object} The current angular namespace + */ +function noConflict() { + var a = window.angular; + window.angular = _angular; + return a; +} /** * @private @@ -89,7 +108,6 @@ function isArrayLike(obj) { } } - /** * @ngdoc function * @name angular.forEach @@ -251,6 +269,11 @@ function inherit(parent, extra) { return extend(new (extend(function() {}, {prototype:parent}))(), extra); } +var START_SPACE = /^\s*/; +var END_SPACE = /\s*$/; +function stripWhitespace(str) { + return isString(str) ? str.replace(START_SPACE, '').replace(END_SPACE, '') : str; +} /** * @ngdoc function @@ -282,7 +305,7 @@ noop.$inject = []; *
      function transformer(transformationFn, value) {
-       return (transformationFn || angular.identity)(value);
+       return (transformationFn || identity)(value);
      };
    
*/ @@ -409,18 +432,6 @@ function isArray(value) { function isFunction(value){return typeof value == 'function';} -/** - * Determines if a value is a regular expression object. - * - * @private - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is a `RegExp`. - */ -function isRegExp(value) { - return toString.apply(value) == '[object RegExp]'; -} - - /** * Checks if `obj` is a window object. * @@ -448,20 +459,9 @@ function isBoolean(value) { } -var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; -})(); - +function trim(value) { + return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; +} /** * @ngdoc function @@ -604,8 +604,6 @@ function copy(source, destination){ destination = copy(source, []); } else if (isDate(source)) { destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source); } else if (isObject(source)) { destination = copy(source, {}); } @@ -653,7 +651,7 @@ function shallowCopy(src, dst) { * @function * * @description - * Determines if two objects or two values are equivalent. Supports value types, regular expressions, arrays and + * Determines if two objects or two values are equivalent. Supports value types, arrays and * objects. * * Two objects or values are considered equivalent if at least one of the following is true: @@ -661,11 +659,8 @@ function shallowCopy(src, dst) { * * Both objects or values pass `===` comparison. * * Both objects or values are of the same type and all of their properties pass `===` comparison. * * Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, - * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual - * representation matches). * - * During a property comparision, properties of `function` type and properties with names + * During a property comparison, properties of `function` type and properties with names * that begin with `$` are ignored. * * Scope and DOMWindow objects are being compared only by identify (`===`). @@ -682,7 +677,6 @@ function equals(o1, o2) { if (t1 == t2) { if (t1 == 'object') { if (isArray(o1)) { - if (!isArray(o2)) return false; if ((length = o1.length) == o2.length) { for(key=0; key @@ -875,12 +848,10 @@ function tryDecodeURIComponent(value) { function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { + if (keyValue) { key_value = keyValue.split('='); - key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { - obj[key] = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - } + key = decodeURIComponent(key_value[0]); + obj[key] = isDefined(key_value[1]) ? decodeURIComponent(key_value[1]) : true; } }); return obj; @@ -896,7 +867,7 @@ function toKeyValue(obj) { /** - * We need our custom method because encodeURIComponent is too agressive and doesn't follow + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path * segments: * segment = *pchar @@ -916,7 +887,7 @@ function encodeUriSegment(val) { /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be + * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" @@ -946,14 +917,10 @@ function encodeUriQuery(val, pctEncodeSpaces) { * @description * * Use this directive to auto-bootstrap an application. Only - * one ngApp directive can be used per HTML document. The directive + * one directive can be used per HTML document. The directive * designates the root of the application and is typically placed * at the root of the page. * - * The first ngApp found in the document will be auto-bootstrapped. To use multiple applications in an - * HTML document you must manually bootstrap them using {@link angular.bootstrap}. - * Applications cannot be nested. - * * In the example below if the `ngApp` directive would not be placed * on the `html` element then the document would not be compiled * and the `{{ 1+2 }}` would not be resolved to `3`. @@ -1019,15 +986,12 @@ function angularInit(element, bootstrap) { * * See: {@link guide/bootstrap Bootstrap} * - * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link api/ng.directive:ngApp ngApp}. - * * @param {Element} element DOM element which is the root of angular application. * @param {Array=} modules an array of module declarations. See: {@link angular.module modules} * @returns {AUTO.$injector} Returns the newly created injector for this app. */ function bootstrap(element, modules) { - var doBootstrap = function() { + var resumeBootstrapInternal = function() { element = jqLite(element); modules = modules || []; modules.unshift(['$provide', function($provide) { @@ -1035,12 +999,13 @@ function bootstrap(element, modules) { }]); modules.unshift('ng'); var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', - function(scope, element, compile, injector) { + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animator', + function(scope, element, compile, injector, animator) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); }); + animator.enabled(true); }] ); return injector; @@ -1049,7 +1014,7 @@ function bootstrap(element, modules) { var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { - return doBootstrap(); + return resumeBootstrapInternal(); } window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); @@ -1057,7 +1022,7 @@ function bootstrap(element, modules) { forEach(extraModules, function(module) { modules.push(module); }); - doBootstrap(); + resumeBootstrapInternal(); }; } @@ -1081,10 +1046,9 @@ function bindJQuery() { injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - JQLitePatchJQueryRemove('remove', true, true, false); - JQLitePatchJQueryRemove('empty', false, false, false); - JQLitePatchJQueryRemove('html', false, false, true); + JQLitePatchJQueryRemove('remove', true); + JQLitePatchJQueryRemove('empty'); + JQLitePatchJQueryRemove('html'); } else { jqLite = JQLite; } @@ -1111,33 +1075,6 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { return arg; } -/** - * Return the value accessible from the object by path. Any undefined traversals are ignored - * @param {Object} obj starting object - * @param {string} path path to traverse - * @param {boolean=true} bindFnToScope - * @returns value as accessible by path - */ -//TODO(misko): this function needs to be removed -function getter(obj, path, bindFnToScope) { - if (!path) return obj; - var keys = path.split('.'); - var key; - var lastInstance = obj; - var len = keys.length; - - for (var i = 0; i < len; i++) { - key = keys[i]; - if (obj) { - obj = (lastInstance = obj)[key]; - } - } - if (!bindFnToScope && isFunction(obj)) { - return bind(lastInstance, obj); - } - return obj; -} - /** * @ngdoc interface * @name angular.Module @@ -1168,8 +1105,8 @@ function setupModuleLoader(window) { * * # Module * - * A module is a collection of services, directives, filters, and configuration information. - * `angular.module` is used to configure the {@link AUTO.$injector $injector}. + * A module is a collocation of services, directives, filters, and configuration information. Module + * is used to configure the {@link AUTO.$injector $injector}. * *
      * // Create a new module
@@ -1301,6 +1238,33 @@ function setupModuleLoader(window) {
            */
           constant: invokeLater('$provide', 'constant', 'unshift'),
 
+          /**
+           * @ngdoc method
+           * @name angular.Module#animation
+           * @methodOf angular.Module
+           * @param {string} name animation name
+           * @param {Function} animationFactory Factory function for creating new instance of an animation.
+           * @description
+           *
+           * Defines an animation hook that can be later used with {@link ng.directive:ngAnimate ngAnimate}
+           * alongside {@link ng.directive:ngAnimate#Description common ng directives} as well as custom directives.
+           * 
+           * module.animation('animation-name', function($inject1, $inject2) {
+           *   return {
+           *     //this gets called in preparation to setup an animation
+           *     setup : function(element) { ... },
+           *
+           *     //this gets called once the animation is run
+           *     start : function(element, done, memo) { ... }
+           *   }
+           * })
+           * 
+ * + * See {@link ng.$animationProvider#register $animationProvider.register()} and + * {@link ng.directive:ngAnimate ngAnimate} for more information. + */ + animation: invokeLater('$animationProvider', 'register'), + /** * @ngdoc method * @name angular.Module#filter @@ -1393,18 +1357,18 @@ function setupModuleLoader(window) { * 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". - * - `minor` – `{number}` – Minor version number, such as "9". - * - `dot` – `{number}` – Dot version number, such as "18". - * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". + * - `full` – `{string}` – Full version string, such as "0.9.18". + * - `major` – `{number}` – Major version number, such as "0". + * - `minor` – `{number}` – Minor version number, such as "9". + * - `dot` – `{number}` – Dot version number, such as "18". + * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.0.8', // all of these placeholder strings will be replaced by grunt's + full: '1.1.5', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 0, - dot: 8, - codeName: 'bubble-burst' + minor: 1, + dot: 5, + codeName: 'triangle-squarification' }; @@ -1434,7 +1398,8 @@ function publishExternalAPI(angular){ 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0} + 'callbacks': {counter: 0}, + 'noConflict': noConflict }); angularModule = setupModuleLoader(window); @@ -1467,12 +1432,14 @@ function publishExternalAPI(angular){ ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, + ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, ngPluralize: ngPluralizeDirective, ngRepeat: ngRepeatDirective, ngShow: ngShowDirective, + ngSubmit: ngSubmitDirective, ngStyle: ngStyleDirective, ngSwitch: ngSwitchDirective, ngSwitchWhen: ngSwitchWhenDirective, @@ -1491,6 +1458,8 @@ function publishExternalAPI(angular){ directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, + $animation: $AnimationProvider, + $animator: $AnimatorProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, @@ -1542,8 +1511,7 @@ function publishExternalAPI(angular){ * Note: All element references in Angular are always wrapped with jQuery or jqLite; they are never * raw DOM references. * - * ## Angular's jqLite - * Angular's lite version of jQuery provides only the following jQuery methods: + * ## Angular's jQuery lite provides the following methods: * * - [addClass()](http://api.jquery.com/addClass/) * - [after()](http://api.jquery.com/after/) @@ -1571,19 +1539,13 @@ function publishExternalAPI(angular){ * - [replaceWith()](http://api.jquery.com/replaceWith/) * - [text()](http://api.jquery.com/text/) * - [toggleClass()](http://api.jquery.com/toggleClass/) - * - [triggerHandler()](http://api.jquery.com/triggerHandler/) - Doesn't pass native event objects to handlers. + * - [triggerHandler()](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. * - [unbind()](http://api.jquery.com/unbind/) - Does not support namespaces * - [val()](http://api.jquery.com/val/) * - [wrap()](http://api.jquery.com/wrap/) * - * ## jQuery/jqLite Extras - * Angular also provides the following additional methods and events to both jQuery and jqLite: + * ## In addition to the above, Angular provides additional methods to both jQuery and jQuery lite: * - * ### Events - * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event - * on all DOM nodes being removed. This can be used to clean up and 3rd party bindings to the DOM - * element before it is removed. - * ### Methods * - `controller(name)` - retrieves the controller of the current element or its parent. By default * retrieves controller associated with the `ngController` directive. If `name` is provided as * camelCase directive name, then the controller for this directive will be retrieved (e.g. @@ -1630,38 +1592,37 @@ function camelCase(name) { ///////////////////////////////////////////// // jQuery mutation patch // -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a +// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a // $destroy event on all DOM nodes being removed. // ///////////////////////////////////////////// -function JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { +function JQLitePatchJQueryRemove(name, dispatchThis) { var originalJqFn = jQuery.fn[name]; originalJqFn = originalJqFn.$original || originalJqFn; removePatch.$original = originalJqFn; jQuery.fn[name] = removePatch; - function removePatch(param) { - var list = filterElems && param ? [this.filter(param)] : [this], + function removePatch() { + var list = [this], fireEvent = dispatchThis, set, setIndex, setLength, - element, childIndex, childLength, children; + element, childIndex, childLength, children, + fns, events; - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } + while(list.length) { + set = list.shift(); + for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { + element = jqLite(set[setIndex]); + if (fireEvent) { + element.triggerHandler('$destroy'); + } else { + fireEvent = !fireEvent; + } + for(childIndex = 0, childLength = (children = element.children()).length; + childIndex < childLength; + childIndex++) { + list.push(jQuery(children[childIndex])); } } } @@ -1721,7 +1682,7 @@ function JQLiteUnbind(element, type, fn) { removeEventListenerFn(element, type, events[type]); delete events[type]; } else { - arrayRemove(events[type] || [], fn); + arrayRemove(events[type], fn); } } } @@ -1851,9 +1812,14 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - JQLite(window).bind('load', trigger); // fallback to window.onload for others + // check if document already is loaded + if (document.readyState === 'complete'){ + setTimeout(trigger); + } else { + this.bind('DOMContentLoaded', trigger); // works for modern browsers and IE9 + // we can not use jqLite since we are not done loading and jQuery could be loaded later. + JQLite(window).bind('load', trigger); // fallback to window.onload for others + } }, toString: function() { var value = []; @@ -1877,11 +1843,11 @@ var JQLitePrototype = JQLite.prototype = { // value on get. ////////////////////////////////////////// var BOOLEAN_ATTR = {}; -forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value) { +forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { BOOLEAN_ATTR[lowercase(value)] = value; }); var BOOLEAN_ELEMENTS = {}; -forEach('input,select,option,textarea,button,form'.split(','), function(value) { +forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { BOOLEAN_ELEMENTS[uppercase(value)] = true; }); @@ -1995,15 +1961,6 @@ forEach({ val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { - var result = []; - forEach(element.options, function (option) { - if (option.selected) { - result.push(option.value || option.text); - } - }); - return result.length === 0 ? null : result; - } return element.value; } element.value = value; @@ -2088,7 +2045,7 @@ function createEventHandler(element, events) { } event.isDefaultPrevented = function() { - return event.defaultPrevented; + return event.defaultPrevented || event.returnValue == false; }; forEach(events[type || event.type], function(fn) { @@ -2212,8 +2169,9 @@ forEach({ append: function(element, node) { forEach(new JQLite(node), function(child){ - if (element.nodeType === 1) + if (element.nodeType === 1 || element.nodeType === 11) { element.appendChild(child); + } }); }, @@ -2221,7 +2179,12 @@ forEach({ if (element.nodeType === 1) { var index = element.firstChild; forEach(new JQLite(node), function(child){ - element.insertBefore(child, index); + if (index) { + element.insertBefore(child, index); + } else { + element.appendChild(child); + index = child; + } }); } }, @@ -2285,9 +2248,10 @@ forEach({ triggerHandler: function(element, eventName) { var eventFns = (JQLiteExpandoStore(element, 'events') || {})[eventName]; + var event; forEach(eventFns, function(fn) { - fn.call(element, null); + fn.call(element, {preventDefault: noop}); }); } }, function(fn, name){ @@ -2376,50 +2340,6 @@ HashMap.prototype = { } }; -/** - * A map where multiple values can be added to the same key such that they form a queue. - * @returns {HashQueueMap} - */ -function HashQueueMap() {} -HashQueueMap.prototype = { - /** - * Same as array push, but using an array as the value for the hash - */ - push: function(key, value) { - var array = this[key = hashKey(key)]; - if (!array) { - this[key] = [value]; - } else { - array.push(value); - } - }, - - /** - * Same as array shift, but using an array as the value for the hash - */ - shift: function(key) { - var array = this[key = hashKey(key)]; - if (array) { - if (array.length == 1) { - delete this[key]; - return array[0]; - } else { - return array.shift(); - } - } - }, - - /** - * return the first item without deleting it - */ - peek: function(key) { - var array = this[hashKey(key)]; - if (array) { - return array[0]; - } - } -}; - /** * @ngdoc function * @name angular.injector @@ -2571,6 +2491,18 @@ function annotate(fn) { * @returns {*} the value returned by the invoked `fn` function. */ +/** + * @ngdoc method + * @name AUTO.$injector#has + * @methodOf AUTO.$injector + * + * @description + * Allows the user to query if the particular service exist. + * + * @param {string} Name of the service to query. + * @returns {boolean} returns true if injector has given service. + */ + /** * @ngdoc method * @name AUTO.$injector#instantiate @@ -2828,9 +2760,10 @@ function createInjector(modulesToLoad) { decorator: decorator } }, - providerInjector = createInternalInjector(providerCache, function() { - throw Error("Unknown provider: " + path.join(' <- ')); - }), + providerInjector = (providerCache.$injector = + createInternalInjector(providerCache, function() { + throw Error("Unknown provider: " + path.join(' <- ')); + })), instanceCache = {}, instanceInjector = (instanceCache.$injector = createInternalInjector(instanceCache, function(servicename) { @@ -2907,9 +2840,7 @@ function createInjector(modulesToLoad) { try { for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { var invokeArgs = invokeQueue[i], - provider = invokeArgs[0] == '$injector' - ? providerInjector - : providerInjector.get(invokeArgs[0]); + provider = providerInjector.get(invokeArgs[0]); provider[invokeArgs[1]].apply(provider, invokeArgs[2]); } @@ -3018,7 +2949,10 @@ function createInjector(modulesToLoad) { invoke: invoke, instantiate: instantiate, get: getService, - annotate: annotate + annotate: annotate, + has: function(name) { + return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + } }; } } @@ -3090,6 +3024,506 @@ function $AnchorScrollProvider() { }]; } + +/** + * @ngdoc object + * @name ng.$animationProvider + * @description + * + * The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside + * of a module. + * + */ +$AnimationProvider.$inject = ['$provide']; +function $AnimationProvider($provide) { + var suffix = 'Animation'; + + /** + * @ngdoc function + * @name ng.$animation#register + * @methodOf ng.$animationProvider + * + * @description + * Registers a new injectable animation factory function. The factory function produces the animation object which + * has these two properties: + * + * * `setup`: `function(Element):*` A function which receives the starting state of the element. The purpose + * of this function is to get the element ready for animation. Optionally the function returns an memento which + * is passed to the `start` function. + * * `start`: `function(Element, doneFunction, *)` The element to animate, the `doneFunction` to be called on + * element animation completion, and an optional memento from the `setup` function. + * + * @param {string} name The name of the animation. + * @param {function} factory The factory function that will be executed to return the animation object. + * + */ + this.register = function(name, factory) { + $provide.factory(camelCase(name) + suffix, factory); + }; + + this.$get = ['$injector', function($injector) { + /** + * @ngdoc function + * @name ng.$animation + * @function + * + * @description + * The $animation service is used to retrieve any defined animation functions. When executed, the $animation service + * will return a object that contains the setup and start functions that were defined for the animation. + * + * @param {String} name Name of the animation function to retrieve. Animation functions are registered and stored + * inside of the AngularJS DI so a call to $animate('custom') is the same as injecting `customAnimation` + * via dependency injection. + * @return {Object} the animation object which contains the `setup` and `start` functions that perform the animation. + */ + return function $animation(name) { + if (name) { + var animationName = camelCase(name) + suffix; + if ($injector.has(animationName)) { + return $injector.get(animationName); + } + } + }; + }]; +} + +// NOTE: this is a pseudo directive. + +/** + * @ngdoc directive + * @name ng.directive:ngAnimate + * + * @description + * The `ngAnimate` directive works as an attribute that is attached alongside pre-existing directives. + * It effects how the directive will perform DOM manipulation. This allows for complex animations to take place + * without burdening the directive which uses the animation with animation details. The built in directives + * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView` already accept `ngAnimate` directive. + * Custom directives can take advantage of animation through {@link ng.$animator $animator service}. + * + * Below is a more detailed breakdown of the supported callback events provided by pre-exisitng ng directives: + * + * | Directive | Supported Animations | + * |========================================================== |====================================================| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ng.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide | + * + * You can find out more information about animations upon visiting each directive page. + * + * Below is an example of a directive that makes use of the ngAnimate attribute: + * + *
+ * 
+ * 
+ *
+ * 
+ * 
+ * 
+ * 
+ *
+ * 
+ * 
+ * 
+ * + * The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned. + * + * Keep in mind that if an animation is running, no child element of such animation can also be animated. + * + *

CSS-defined Animations

+ * By default, ngAnimate attaches two CSS classes per animation event to the DOM element to achieve the animation. + * It is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions as + * well as CSS animations. + * + * The following code below demonstrates how to perform animations using **CSS transitions** with ngAnimate: + * + *
+ * 
+ *
+ * 
+ *
+ * + * The following code below demonstrates how to perform animations using **CSS animations** with ngAnimate: + * + *
+ * 
+ *
+ * 
+ *
+ * + * ngAnimate will first examine any CSS animation code and then fallback to using CSS transitions. + * + * Upon DOM mutation, the event class is added first, then the browser is allowed to reflow the content and then, + * the active class is added to trigger the animation. The ngAnimate directive will automatically extract the duration + * of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be + * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end + * immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element + * has no CSS transition/animation classes surrounding it. + * + *

JavaScript-defined Animations

+ * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations to browsers that do not + * yet support them, then you can make use of JavaScript animations defined inside of your AngularJS module. + * + *
+ * var ngModule = angular.module('YourApp', []);
+ * ngModule.animation('animate-enter', function() {
+ *   return {
+ *     setup : function(element) {
+ *       //prepare the element for animation
+ *       element.css({ 'opacity': 0 });
+ *       var memo = "..."; //this value is passed to the start function
+ *       return memo;
+ *     },
+ *     start : function(element, done, memo) {
+ *       //start the animation
+ *       element.animate({
+ *         'opacity' : 1
+ *       }, function() {
+ *         //call when the animation is complete
+ *         done()
+ *       });
+ *     }
+ *   }
+ * });
+ * 
+ * + * As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation + * can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled + * animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're not using + * CSS animations) to animated the element, but it will not attempt to find any CSS3 transition or animation duration/delay values. + * It will instead close off the animation once the provided done function is executed. So it's important that you + * make sure your animations remember to fire off the done function once the animations are complete. + * + * @param {expression} ngAnimate Used to configure the DOM manipulation animations. + * + */ + +var $AnimatorProvider = function() { + var NG_ANIMATE_CONTROLLER = '$ngAnimateController'; + var rootAnimateController = {running:true}; + + this.$get = ['$animation', '$window', '$sniffer', '$rootElement', '$rootScope', + function($animation, $window, $sniffer, $rootElement, $rootScope) { + $rootElement.data(NG_ANIMATE_CONTROLLER, rootAnimateController); + + /** + * @ngdoc function + * @name ng.$animator + * @function + * + * @description + * The $animator.create service provides the DOM manipulation API which is decorated with animations. + * + * @param {Scope} scope the scope for the ng-animate. + * @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are + * passed into the linking function of the directive using the `$animator`.) + * @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods. + */ + var AnimatorService = function(scope, attrs) { + var animator = {}; + + /** + * @ngdoc function + * @name ng.animator#enter + * @methodOf ng.$animator + * @function + * + * @description + * Injects the element object into the DOM (inside of the parent element) and then runs the enter animation. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation + */ + animator.enter = animateActionFactory('enter', insert, noop); + + /** + * @ngdoc function + * @name ng.animator#leave + * @methodOf ng.$animator + * @function + * + * @description + * Runs the leave animation operation and, upon completion, removes the element from the DOM. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation + */ + animator.leave = animateActionFactory('leave', noop, remove); + + /** + * @ngdoc function + * @name ng.animator#move + * @methodOf ng.$animator + * @function + * + * @description + * Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or + * add the element directly after the after element if present. Then the move animation will be run. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the move animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation + */ + animator.move = animateActionFactory('move', move, noop); + + /** + * @ngdoc function + * @name ng.animator#show + * @methodOf ng.$animator + * @function + * + * @description + * Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + */ + animator.show = animateActionFactory('show', show, noop); + + /** + * @ngdoc function + * @name ng.animator#hide + * @methodOf ng.$animator + * + * @description + * Starts the hide animation first and sets the CSS `display` property to `none` upon completion. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + */ + animator.hide = animateActionFactory('hide', noop, hide); + + /** + * @ngdoc function + * @name ng.animator#animate + * @methodOf ng.$animator + * + * @description + * Triggers a custom animation event to be executed on the given element + * + * @param {jQuery/jqLite element} element that will be animated + */ + animator.animate = function(event, element) { + animateActionFactory(event, noop, noop)(element); + } + return animator; + + function animateActionFactory(type, beforeFn, afterFn) { + return function(element, parent, after) { + var ngAnimateValue = scope.$eval(attrs.ngAnimate); + var className = ngAnimateValue + ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type + : ''; + var animationPolyfill = $animation(className); + var polyfillSetup = animationPolyfill && animationPolyfill.setup; + var polyfillStart = animationPolyfill && animationPolyfill.start; + var polyfillCancel = animationPolyfill && animationPolyfill.cancel; + + if (!className) { + beforeFn(element, parent, after); + afterFn(element, parent, after); + } else { + var activeClassName = className + '-active'; + + if (!parent) { + parent = after ? after.parent() : element.parent(); + } + if ((!$sniffer.transitions && !polyfillSetup && !polyfillStart) || + (parent.inheritedData(NG_ANIMATE_CONTROLLER) || noop).running) { + beforeFn(element, parent, after); + afterFn(element, parent, after); + return; + } + + var animationData = element.data(NG_ANIMATE_CONTROLLER) || {}; + if(animationData.running) { + (polyfillCancel || noop)(element); + animationData.done(); + } + + element.data(NG_ANIMATE_CONTROLLER, {running:true, done:done}); + element.addClass(className); + beforeFn(element, parent, after); + if (element.length == 0) return done(); + + var memento = (polyfillSetup || noop)(element); + + // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate + // keep at 1 for animation dom rerender + $window.setTimeout(beginAnimation, 1); + } + + function parseMaxTime(str) { + var total = 0, values = isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } + + function beginAnimation() { + element.addClass(activeClassName); + if (polyfillStart) { + polyfillStart(element, done, memento); + } else if (isFunction($window.getComputedStyle)) { + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; + + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount', + duration = 0; + + //we want all the styles defined before and after + var ELEMENT_NODE = 1; + forEach(element, function(element) { + if (element.nodeType == ELEMENT_NODE) { + var w3cProp = w3cTransitionProp, + vendorProp = vendorTransitionProp, + iterations = 1, + elementStyles = $window.getComputedStyle(element) || {}; + + //use CSS Animations over CSS Transitions + if(parseFloat(elementStyles[w3cAnimationProp + durationKey]) > 0 || + parseFloat(elementStyles[vendorAnimationProp + durationKey]) > 0) { + w3cProp = w3cAnimationProp; + vendorProp = vendorAnimationProp; + iterations = Math.max(parseInt(elementStyles[w3cProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorProp + animationIterationCountKey]) || 0, + iterations); + } + + var parsedDelay = Math.max(parseMaxTime(elementStyles[w3cProp + delayKey]), + parseMaxTime(elementStyles[vendorProp + delayKey])); + + var parsedDuration = Math.max(parseMaxTime(elementStyles[w3cProp + durationKey]), + parseMaxTime(elementStyles[vendorProp + durationKey])); + + duration = Math.max(parsedDelay + (iterations * parsedDuration), duration); + } + }); + $window.setTimeout(done, duration * 1000); + } else { + done(); + } + } + + function done() { + if(!done.run) { + done.run = true; + afterFn(element, parent, after); + element.removeClass(className); + element.removeClass(activeClassName); + element.removeData(NG_ANIMATE_CONTROLLER); + } + } + }; + } + + function show(element) { + element.css('display', ''); + } + + function hide(element) { + element.css('display', 'none'); + } + + function insert(element, parent, after) { + if (after) { + after.after(element); + } else { + parent.append(element); + } + } + + function remove(element) { + element.remove(); + } + + function move(element, parent, after) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + insert(element, parent, after); + } + }; + + /** + * @ngdoc function + * @name ng.animator#enabled + * @methodOf ng.$animator + * @function + * + * @param {Boolean=} If provided then set the animation on or off. + * @return {Boolean} Current animation state. + * + * @description + * Globally enables/disables animations. + * + */ + AnimatorService.enabled = function(value) { + if (arguments.length) { + rootAnimateController.running = !value; + } + return !rootAnimateController.running; + }; + + return AnimatorService; + }]; +}; + /** * ! This is a private undocumented service ! * @@ -3214,8 +3648,7 @@ function Browser(window, document, $log, $sniffer) { ////////////////////////////////////////////////////////////// var lastBrowserUrl = location.href, - baseElement = document.find('base'), - replacedUrl = null; + baseElement = document.find('base'); /** * @name ng.$browser#url @@ -3250,21 +3683,14 @@ function Browser(window, document, $log, $sniffer) { baseElement.attr('href', baseElement.attr('href')); } } else { - if (replace) { - location.replace(url); - replacedUrl = url; - } else { - location.href = url; - replacedUrl = null; - } + if (replace) location.replace(url); + else location.href = url; } return self; // getter } else { - // - the replacedUrl is a workaround for an IE8-9 issue with location.replace method that doesn't update - // location.href synchronously - // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return replacedUrl || location.href.replace(/%27/g,"'"); + // the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 + return location.href.replace(/%27/g,"'"); } }; @@ -3350,7 +3776,7 @@ function Browser(window, document, $log, $sniffer) { * @methodOf ng.$browser * * @param {string=} name Cookie name - * @param {string=} value Cokkie value + * @param {string=} value Cookie value * * @description * The cookies method provides a 'private' low level access to browser cookies. @@ -3418,7 +3844,7 @@ function Browser(window, document, $log, $sniffer) { * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. * * @description - * Executes a fn asynchroniously via `setTimeout(fn, delay)`. + * Executes a fn asynchronously via `setTimeout(fn, delay)`. * * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed @@ -3445,7 +3871,7 @@ function Browser(window, document, $log, $sniffer) { * Cancels a defered task identified with `deferId`. * * @param {*} deferId Token returned by the `$browser.defer` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully canceled. */ self.defer.cancel = function(deferId) { if (pendingDeferIds[deferId]) { @@ -3471,35 +3897,22 @@ function $BrowserProvider(){ * @name ng.$cacheFactory * * @description - * Factory that constructs cache objects and gives access to them. - * - *
- *
- *  var cache = $cacheFactory('cacheId');
- *  expect($cacheFactory.get('cacheId')).toBe(cache);
- *  expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
- *
- *  cache.put("key", "value");
- *  cache.put("another key", "another value");
- *
- *  expect(cache.info()).toEqual({id: 'cacheId', size: 2}); // Since we've specified no options on creation
- *
- * 
+ * Factory that constructs cache objects. * * * @param {string} cacheId Name or id of the newly created cache. * @param {object=} options Options object that specifies the cache behavior. Properties: * - * - `{number=}` `capacity` — turns the cache into LRU cache. + * - `{number=}` `capacity` — turns the cache into LRU cache. * * @returns {object} Newly created cache object with the following set of methods: * - * - `{object}` `info()` — Returns id, size, and options of cache. - * - `{void}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache. - * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. - * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. - * - `{void}` `removeAll()` — Removes all cached values. - * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. + * - `{object}` `info()` — Returns id, size, and options of cache. + * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns it. + * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. + * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. + * - `{void}` `removeAll()` — Removes all cached values. + * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * */ function $CacheFactoryProvider() { @@ -3534,6 +3947,8 @@ function $CacheFactoryProvider() { if (size > capacity) { this.remove(staleEnd.key); } + + return value; }, @@ -3616,16 +4031,6 @@ function $CacheFactoryProvider() { } - /** - * @ngdoc method - * @name ng.$cacheFactory#info - * @methodOf ng.$cacheFactory - * - * @description - * Get information about all the of the caches that have been created - * - * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` - */ cacheFactory.info = function() { var info = {}; forEach(caches, function(cache, cacheId) { @@ -3635,17 +4040,6 @@ function $CacheFactoryProvider() { }; - /** - * @ngdoc method - * @name ng.$cacheFactory#get - * @methodOf ng.$cacheFactory - * - * @description - * Get access to a cache object by the `cacheId` used when it was created. - * - * @param {string} cacheId Name or id of a cache to access. - * @returns {object} Cache object identified by the cacheId or undefined if no such cache. - */ cacheFactory.get = function(cacheId) { return caches[cacheId]; }; @@ -3660,43 +4054,7 @@ function $CacheFactoryProvider() { * @name ng.$templateCache * * @description - * The first time a template is used, it is loaded in the template cache for quick retrieval. You can - * load templates directly into the cache in a `script` tag, or by consuming the `$templateCache` - * service directly. - * - * Adding via the `script` tag: - *
- * 
- * 
- * 
- * 
- *   ...
- * 
- * 
- * - * **Note:** the `script` tag containing the template does not need to be included in the `head` of the document, but - * it must be below the `ng-app` definition. - * - * Adding via the $templateCache service: - * - *
- * var myApp = angular.module('myApp', []);
- * myApp.run(function($templateCache) {
- *   $templateCache.put('templateId.html', 'This is the content of the template');
- * });
- * 
- * - * To retrieve the template later, simply use it in your HTML: - *
- * 
- *
- * - * or get it via Javascript: - *
- * $templateCache.get('templateId.html')
- * 
+ * Cache used for storing html templates. * * See {@link ng.$cacheFactory $cacheFactory}. * @@ -3873,11 +4231,11 @@ function $CompileProvider($provide) { * @function * * @description - * Register a new directive with the compiler. + * Register a new directives with the compiler. * * @param {string} name Name of the directive in camel-case. (ie ngBind which will match as * ng-bind). - * @param {function|Array} directiveFactory An injectable directive factory function. See {@link guide/directive} for more + * @param {function} directiveFactory An injectable directive factory function. See {@link guide/directive} for more * info. * @returns {ng.$compileProvider} Self for chaining. */ @@ -4000,7 +4358,7 @@ function $CompileProvider($provide) { // href property always returns normalized absolute url, so we can match against that normalizedVal = urlSanitizationNode.href; - if (normalizedVal !== '' && !normalizedVal.match(urlSanitizationWhitelist)) { + if (!normalizedVal.match(urlSanitizationWhitelist)) { this[key] = value = 'unsafe:' + normalizedVal; } } @@ -4056,7 +4414,8 @@ function $CompileProvider($provide) { ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); - }; + }, + NG_ATTR_BINDING = /^ngAttr[A-Z]/; return compile; @@ -4221,11 +4580,16 @@ function $CompileProvider($provide) { directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority); // iterate over the attributes - for (var attr, name, nName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { + if (attr.specified) { name = attr.name; + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (NG_ATTR_BINDING.test(ngAttrName)) { + name = ngAttrName.substr(6).toLowerCase(); + } nName = directiveNormalize(name.toLowerCase()); attrsMap[nName] = name; attrs[nName] = value = trim((msie && name == 'href') @@ -4353,9 +4717,14 @@ function $CompileProvider($provide) { } } - if ((directiveValue = directive.template)) { + if (directive.template) { assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; + + directiveValue = (isFunction(directive.template)) + ? directive.template($compileNode, templateAttrs) + : directive.template; + directiveValue = denormalizeTemplate(directiveValue); if (directive.replace) { @@ -4475,13 +4844,14 @@ function $CompileProvider($provide) { $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; var parentScope = scope.$parent || scope; forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) { var match = definiton.match(LOCAL_REGEXP) || [], - attrName = match[2]|| scopeName, + attrName = match[3] || scopeName, + optional = (match[2] == '?'), mode = match[1], // @, =, or & lastValue, parentGet, parentSet; @@ -4495,10 +4865,17 @@ function $CompileProvider($provide) { scope[scopeName] = value; }); attrs.$$observers[attrName].$$scope = parentScope; + 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 + scope[scopeName] = $interpolate(attrs[attrName])(parentScope); + } break; } case '=': { + if (optional && !attrs[attrName]) { + return; + } parentGet = $parse(attrs[attrName]); parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest @@ -4670,11 +5047,14 @@ function $CompileProvider($provide) { // The fact that we have to copy and patch the directive seems wrong! derivedSyncDirective = extend({}, origAsyncDirective, { controller: null, templateUrl: null, transclude: null, scope: null - }); + }), + templateUrl = (isFunction(origAsyncDirective.templateUrl)) + ? origAsyncDirective.templateUrl($compileNode, tAttrs) + : origAsyncDirective.templateUrl; $compileNode.html(''); - $http.get(origAsyncDirective.templateUrl, {cache: $templateCache}). + $http.get(templateUrl, {cache: $templateCache}). success(function(content) { var compileNode, tempTemplateAttrs, $template; @@ -4703,10 +5083,10 @@ function $CompileProvider($provide) { while(linkQueue.length) { - var controller = linkQueue.pop(), - linkRootElement = linkQueue.pop(), - beforeTemplateLinkNode = linkQueue.pop(), - scope = linkQueue.pop(), + var scope = linkQueue.shift(), + beforeTemplateLinkNode = linkQueue.shift(), + linkRootElement = linkQueue.shift(), + controller = linkQueue.shift(), linkNode = compileNode; if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { @@ -4787,13 +5167,15 @@ function $CompileProvider($provide) { compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = {})); - if (name === 'class') { - // we need to interpolate classes again, in the case the element was replaced - // and therefore the two class attrs got merged - we want to interpolate the result - interpolateFn = $interpolate(attr[name], true); - } + // we need to interpolate again, in case the attribute value has been updated + // (e.g. by another directive's compile function) + interpolateFn = $interpolate(attr[name], true); - attr[name] = undefined; + // if attribute was updated so that there is no interpolation going on we don't want to + // register any observers + if (!interpolateFn) return; + + attr[name] = interpolateFn(scope); ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(value) { @@ -4888,7 +5270,7 @@ function directiveNormalize(name) { * @param {string} name Normalized element attribute name of the property to modify. The name is * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr} * property to the original name. - * @param {string} value Value to set the attribute to. + * @param {string} value Value to set the attribute to. The value can be an interpolated string. */ @@ -4923,7 +5305,8 @@ function directiveLinkingFn( * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { - var controllers = {}; + var controllers = {}, + CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; /** @@ -4968,17 +5351,32 @@ function $ControllerProvider() { * a service, so that one can override this service with {@link https://gist.github.com/1649788 * BC version}. */ - return function(constructor, locals) { - if(isString(constructor)) { - var name = constructor; - constructor = controllers.hasOwnProperty(name) - ? controllers[name] - : getter(locals.$scope, name, true) || getter($window, name, true); + return function(expression, locals) { + var instance, match, constructor, identifier; - assertArgFn(constructor, name, true); + if(isString(expression)) { + match = expression.match(CNTRL_REG), + constructor = match[1], + identifier = match[3]; + expression = controllers.hasOwnProperty(constructor) + ? controllers[constructor] + : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + + assertArgFn(expression, constructor, true); } - return $injector.instantiate(constructor, locals); + instance = $injector.instantiate(expression, locals); + + if (identifier) { + if (typeof locals.$scope !== 'object') { + throw new Error('Can not export controller as "' + identifier + '". ' + + 'No scope object provided!'); + } + + locals.$scope[identifier] = instance; + } + + return instance; }; }]; } @@ -5076,7 +5474,7 @@ function $InterpolateProvider() { }; - this.$get = ['$parse', function($parse) { + this.$get = ['$parse', '$exceptionHandler', function($parse, $exceptionHandler) { var startSymbolLength = startSymbol.length, endSymbolLength = endSymbol.length; @@ -5148,18 +5546,24 @@ function $InterpolateProvider() { if (!mustHaveExpression || hasInterpolation) { concat.length = length; fn = function(context) { - for(var i = 0, ii = length, part; i html5 url - } else { - return composeProtocolHostPort(match.protocol, match.host, match.port) + - pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); - } +function stripHash(url) { + var index = url.indexOf('#'); + return index == -1 ? url : url.substr(0, index); } -function convertToHashbangUrl(url, basePath, hashPrefix) { - var match = matchUrl(url); +function stripFile(url) { + return url.substr(0, stripHash(url).lastIndexOf('/') + 1); +} - // already hashbang url - if (decodeURIComponent(match.path) == basePath && !isUndefined(match.hash) && - match.hash.indexOf(hashPrefix) === 0) { - return url; - // convert html5 url -> hashbang url - } else { - var search = match.search && '?' + match.search || '', - hash = match.hash && '#' + match.hash || '', - pathPrefix = pathPrefixFromBase(basePath), - path = match.path.substr(pathPrefix.length); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'); - } - - return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + - '#' + hashPrefix + path + search + hash; - } +/* return the server only */ +function serverBase(url) { + return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); } /** - * LocationUrl represents an url + * LocationHtml5Url represents an url * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor - * @param {string} url HTML5 url - * @param {string} pathPrefix + * @param {string} appBase application base URL + * @param {string} basePrefix url path prefix */ -function LocationUrl(url, pathPrefix, appBaseUrl) { - pathPrefix = pathPrefix || ''; - +function LocationHtml5Url(appBase, basePrefix) { + basePrefix = basePrefix || ''; + var appBaseNoFile = stripFile(appBase); /** * Parse given html5 (regular) url string into properties * @param {string} newAbsoluteUrl HTML5 url * @private */ - this.$$parse = function(newAbsoluteUrl) { - var match = matchUrl(newAbsoluteUrl, this); - - if (match.path.indexOf(pathPrefix) !== 0) { - throw Error('Invalid url "' + newAbsoluteUrl + '", missing path prefix "' + pathPrefix + '" !'); + this.$$parse = function(url) { + var parsed = {} + matchUrl(url, parsed); + var pathUrl = beginsWith(appBaseNoFile, url); + if (!isString(pathUrl)) { + throw Error('Invalid url "' + url + '", missing path prefix "' + appBaseNoFile + '".'); + } + matchAppUrl(pathUrl, parsed); + extend(this, parsed); + if (!this.$$path) { + this.$$path = '/'; } - - this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); - this.$$search = parseKeyValue(match.search); - this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; this.$$compose(); }; @@ -5343,19 +5724,25 @@ function LocationUrl(url, pathPrefix, appBaseUrl) { hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - pathPrefix + this.$$url; + this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; + this.$$rewrite = function(url) { + var appUrl, prevAppUrl; - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; + if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + prevAppUrl = appUrl; + if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { + return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + } else { + return appBase + prevAppUrl; + } + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { + return appBaseNoFile + appUrl; + } else if (appBaseNoFile == url + '/') { + return appBaseNoFile; } } - - - this.$$parse(url); } @@ -5364,11 +5751,11 @@ function LocationUrl(url, pathPrefix, appBaseUrl) { * This object is exposed as $location service when html5 history api is disabled or not supported * * @constructor - * @param {string} url Legacy url - * @param {string} hashPrefix Prefix for hash part (containing path and search) + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { - var basePath; +function LocationHashbangUrl(appBase, hashPrefix) { + var appBaseNoFile = stripFile(appBase); /** * Parse given hashbang url into properties @@ -5376,24 +5763,16 @@ function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { * @private */ this.$$parse = function(url) { - var match = matchUrl(url, this); - - - if (match.hash && match.hash.indexOf(hashPrefix) !== 0) { - throw Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'); + matchUrl(url, this); + var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + if (!isString(withoutBaseUrl)) { + throw new Error('Invalid url "' + url + '", does not start with "' + appBase + '".'); } - - basePath = match.path + (match.search ? '?' + match.search : ''); - match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); - if (match[1]) { - this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); - } else { - this.$$path = ''; + var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' ? beginsWith(hashPrefix, withoutBaseUrl) : withoutBaseUrl; + if (!isString(withoutHashUrl)) { + throw new Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '".'); } - - this.$$search = parseKeyValue(match[3]); - this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; - + matchAppUrl(withoutHashUrl, this); this.$$compose(); }; @@ -5406,22 +5785,48 @@ function LocationHashbangUrl(url, hashPrefix, appBaseUrl) { hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + - basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); + this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return absoluteLinkUrl; + this.$$rewrite = function(url) { + if(stripHash(appBase) == stripHash(url)) { + return url; } } - - - this.$$parse(url); } -LocationUrl.prototype = { +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is enabled but the browser + * does not support it. + * + * @constructor + * @param {string} appBase application base URL + * @param {string} hashPrefix hashbang prefix + */ +function LocationHashbangInHtml5Url(appBase, hashPrefix) { + LocationHashbangUrl.apply(this, arguments); + + var appBaseNoFile = stripFile(appBase); + + this.$$rewrite = function(url) { + var appUrl; + + if ( appBase == stripHash(url) ) { + return url; + } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { + return appBase + hashPrefix + appUrl; + } else if ( appBaseNoFile === url + '/') { + return appBaseNoFile; + } + } +} + + +LocationHashbangInHtml5Url.prototype = + LocationHashbangUrl.prototype = + LocationHtml5Url.prototype = { /** * Has any change been replacing ? @@ -5603,21 +6008,6 @@ LocationUrl.prototype = { } }; -LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); - -function LocationHashbangInHtml5Url(url, hashPrefix, appBaseUrl, baseExtra) { - LocationHashbangUrl.apply(this, arguments); - - - this.$$rewriteAppUrl = function(absoluteLinkUrl) { - if (absoluteLinkUrl.indexOf(appBaseUrl) == 0) { - return appBaseUrl + baseExtra + '#' + hashPrefix + absoluteLinkUrl.substr(appBaseUrl.length); - } - } -} - -LocationHashbangInHtml5Url.prototype = inherit(LocationHashbangUrl.prototype); - function locationGetter(property) { return function() { return this[property]; @@ -5714,37 +6104,20 @@ function $LocationProvider(){ this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { var $location, - basePath, - pathPrefix, - initUrl = $browser.url(), - initUrlParts = matchUrl(initUrl), - appBaseUrl; + LocationMode, + baseHref = $browser.baseHref(), + initialUrl = $browser.url(), + appBase; if (html5Mode) { - basePath = $browser.baseHref() || '/'; - pathPrefix = pathPrefixFromBase(basePath); - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - pathPrefix + '/'; - - if ($sniffer.history) { - $location = new LocationUrl( - convertToHtml5Url(initUrl, basePath, hashPrefix), - pathPrefix, appBaseUrl); - } else { - $location = new LocationHashbangInHtml5Url( - convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1)); - } + appBase = baseHref ? serverBase(initialUrl) + baseHref : initialUrl; + LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { - appBaseUrl = - composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + - (initUrlParts.path || '') + - (initUrlParts.search ? ('?' + initUrlParts.search) : '') + - '#' + hashPrefix + '/'; - - $location = new LocationHashbangUrl(initUrl, hashPrefix, appBaseUrl); + appBase = stripHash(initialUrl); + LocationMode = LocationHashbangUrl; } + $location = new LocationMode(appBase, '#' + hashPrefix); + $location.$$parse($location.$$rewrite(initialUrl)); $rootElement.bind('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) @@ -5760,22 +6133,24 @@ function $LocationProvider(){ if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } - var absHref = elm.prop('href'), - rewrittenUrl = $location.$$rewriteAppUrl(absHref); + var absHref = elm.prop('href'); + var rewrittenUrl = $location.$$rewrite(absHref); - if (absHref && !elm.attr('target') && rewrittenUrl) { - // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); + if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { event.preventDefault(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if (rewrittenUrl != $browser.url()) { + // update location manually + $location.$$parse(rewrittenUrl); + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; + } } }); // rewrite hashbang url <> html5 url - if ($location.absUrl() != initUrl) { + if ($location.absUrl() != initialUrl) { $browser.url($location.absUrl(), true); } @@ -5860,7 +6235,33 @@ function $LocationProvider(){ */ +/** + * @ngdoc object + * @name ng.$logProvider + * @description + * Use the `$logProvider` to configure how the application logs messages + */ function $LogProvider(){ + var debug = true, + self = this; + + /** + * @ngdoc property + * @name ng.$logProvider#debugEnabled + * @methodOf ng.$logProvider + * @description + * @param {string=} flag enable or disable debug level messages + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.debugEnabled = function(flag) { + if (isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + this.$get = ['$window', function($window){ return { /** @@ -5901,7 +6302,25 @@ function $LogProvider(){ * @description * Write an error message */ - error: consoleLog('error') + error: consoleLog('error'), + + /** + * @ngdoc method + * @name ng.$log#debug + * @methodOf ng.$log + * + * @description + * Write a debug message + */ + debug: (function () { + var fn = consoleLog('debug'); + + return function() { + if (debug) { + fn.apply(self, arguments); + } + } + }()) }; function formatError(arg) { @@ -5960,6 +6379,8 @@ var OPERATORS = { '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, '=':noop, + '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, + '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, '<':function(self, locals, a,b){return a(self, locals) @@ -6881,13 +7395,13 @@ function $ParseProvider() { * return result + 1; * }); * - * // promiseB will be resolved immediately after promiseA is resolved and its value - * // will be the result of promiseA incremented by 1 + * // promiseB will be resolved immediately after promiseA is resolved and its value will be + * // the result of promiseA incremented by 1 *
* * It is possible to create chains of any length and since a promise can be resolved with another * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful APIs like + * the promises at any point in the chain. This makes it possible to implement powerful apis like * $http's response interceptors. * * @@ -6994,8 +7508,8 @@ function qFactory(nextTick, exceptionHandler) { try { result.resolve((callback || defaultCallback)(value)); } catch(e) { - result.reject(e); exceptionHandler(e); + result.reject(e); } }; @@ -7003,8 +7517,8 @@ function qFactory(nextTick, exceptionHandler) { try { result.resolve((errback || defaultErrback)(reason)); } catch(e) { - result.reject(e); exceptionHandler(e); + result.reject(e); } }; @@ -7015,6 +7529,42 @@ function qFactory(nextTick, exceptionHandler) { } return result.promise; + }, + always: function(callback) { + + function makePromise(value, resolved) { + var result = defer(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; + } + + function handleCallback(value, isResolved) { + var callbackOutput = null; + try { + callbackOutput = (callback ||defaultCallback)(); + } catch(e) { + return makePromise(e, false); + } + if (callbackOutput && callbackOutput.then) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + } + + return this.then(function(value) { + return handleCallback(value, true); + }, function(error) { + return handleCallback(error, false); + }); } } }; @@ -7153,29 +7703,30 @@ function qFactory(nextTick, exceptionHandler) { * Combines multiple promises into a single promise that is resolved when all of the input * promises are resolved. * - * @param {Array.} promises An array of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array of values, - * each value corresponding to the promise at the same index in the `promises` array. If any of + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. If any of * the promises is resolved with a rejection, this resulting promise will be resolved with the * same rejection. */ function all(promises) { var deferred = defer(), - counter = promises.length, - results = []; + counter = 0, + results = isArray(promises) ? [] : {}; - if (counter) { - forEach(promises, function(promise, index) { - ref(promise).then(function(value) { - if (index in results) return; - results[index] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (index in results) return; - deferred.reject(reason); - }); + forEach(promises, function(promise, key) { + counter++; + ref(promise).then(function(value) { + if (results.hasOwnProperty(key)) return; + results[key] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (results.hasOwnProperty(key)) return; + deferred.reject(reason); }); - } else { + }); + + if (counter === 0) { deferred.resolve(results); } @@ -7212,38 +7763,59 @@ function $RouteProvider(){ * `$location.path` will be updated to add or drop the trailing slash to exactly match the * route definition. * - * `path` can contain named groups starting with a colon (`:name`). All characters up to the - * next slash are matched and stored in `$routeParams` under the given `name` after the route - * is resolved. + * * `path` can contain named groups starting with a colon (`:name`). All characters up + * to the next slash are matched and stored in `$routeParams` under the given `name` + * when the route matches. + * * `path` can contain named groups starting with a star (`*name`). All characters are + * eagerly stored in `$routeParams` under the given `name` when the route matches. + * + * For example, routes like `/color/:color/largecode/*largecode/edit` will match + * `/color/brown/largecode/code/with/slashs/edit` and extract: + * + * * `color: brown` + * * `largecode: code/with/slashs`. + * * * @param {Object} route Mapping information to be assigned to `$route.current` on route * match. * * Object properties: * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly + * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly * created scope or the name of a {@link angular.Module#controller registered controller} * if passed as a string. - * - `template` – `{string=}` – html template as a string that should be used by - * {@link ng.directive:ngView ngView} or + * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be + * published to scope under the `controllerAs` name. + * - `template` – `{string=|function()=}` – html template as a string or function that returns + * an html template as a string which should be used by {@link ng.directive:ngView ngView} or * {@link ng.directive:ngInclude ngInclude} directives. - * this property takes precedence over `templateUrl`. - * - `templateUrl` – `{string=}` – path to an html template that should be used by - * {@link ng.directive:ngView ngView}. + * This property takes precedence over `templateUrl`. + * + * If `template` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used by {@link ng.directive:ngView ngView}. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, they will be * resolved and converted to a value before the controller is instantiated and the * `$routeChangeSuccess` event is fired. The map object is: * - * - `key` – `{string}`: a name of a dependency to be injected into the controller. + * - `key` – `{string}`: a name of a dependency to be injected into the controller. * - `factory` - `{string|function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected} * and the return value is treated as the dependency. If the result is a promise, it is resolved - * before its value is injected into the controller. Be aware that `ngRoute.$routeParams` will - * still refer to the previous route within these resolve functions. Use `$route.current.params` - * to access the new route parameters, instead. + * before its value is injected into the controller. * - * - `redirectTo` – {(string|function())=} – value to update + * - `redirectTo` – {(string|function())=} – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -7262,13 +7834,18 @@ function $RouteProvider(){ * If the option is set to `false` and url in the browser changes, then * `$routeUpdate` event is broadcasted on the root scope. * + * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * + * If the option is set to `true`, then the particular route can be matched without being + * case sensitive + * * @returns {Object} self * * @description * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { - routes[path] = extend({reloadOnSearch: true}, route); + routes[path] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route); // create redirection for trailing slashes if (path) { @@ -7514,19 +8091,21 @@ function $RouteProvider(){ /** * @param on {string} current url * @param when {string} route when template to match the url against + * @param whenProperties {Object} properties to define when's matching behavior * @return {?Object} */ - function switchRouteMatcher(on, when) { + function switchRouteMatcher(on, when, whenProperties) { // TODO(i): this code is convoluted and inefficient, we should construct the route matching // regex only once and then reuse it // Escape regexp special characters. - when = '^' + when.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") + '$'; + when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, "\\$&") + '$'; + var regex = '', params = [], dst = {}; - var re = /:(\w+)/g, + var re = /\\([:*])(\w+)/g, paramMatch, lastMatchedIndex = 0; @@ -7534,14 +8113,21 @@ function $RouteProvider(){ // Find each :param in `when` and replace it with a capturing group. // Append all other sections of when unchanged. regex += when.slice(lastMatchedIndex, paramMatch.index); - regex += '([^\\/]*)'; - params.push(paramMatch[1]); + switch(paramMatch[1]) { + case ':': + regex += '([^\\/]*)'; + break; + case '*': + regex += '(.*)'; + break; + } + params.push(paramMatch[2]); lastMatchedIndex = re.lastIndex; } // Append trailing path part. regex += when.substr(lastMatchedIndex); - var match = on.match(new RegExp(regex)); + var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : '')); if (match) { forEach(params, function(name, index) { dst[name] = match[index + 1]; @@ -7578,30 +8164,31 @@ function $RouteProvider(){ $q.when(next). then(function() { if (next) { - var keys = [], - values = [], + var locals = extend({}, next.resolve), template; - forEach(next.resolve || {}, function(value, key) { - keys.push(key); - values.push(isString(value) ? $injector.get(value) : $injector.invoke(value)); + forEach(locals, function(value, key) { + locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); }); + if (isDefined(template = next.template)) { + if (isFunction(template)) { + template = template(next.params); + } } else if (isDefined(template = next.templateUrl)) { - template = $http.get(template, {cache: $templateCache}). - then(function(response) { return response.data; }); + if (isFunction(template)) { + template = template(next.params); + } + if (isDefined(template)) { + next.loadedTemplateUrl = template; + template = $http.get(template, {cache: $templateCache}). + then(function(response) { return response.data; }); + } } if (isDefined(template)) { - keys.push('$template'); - values.push(template); + locals['$template'] = template; } - return $q.all(values).then(function(values) { - var locals = {}; - forEach(values, function(value, index) { - locals[keys[index]] = value; - }); - return locals; - }); + return $q.all(locals); } }). // after route change @@ -7629,7 +8216,7 @@ function $RouteProvider(){ // Match a route var params, match; forEach(routes, function(route, path) { - if (!match && (params = switchRouteMatcher($location.path(), path))) { + if (!match && (params = switchRouteMatcher($location.path(), path, route))) { match = inherit(route, { params: extend({}, $location.search(), params), pathParams: params}); @@ -7641,7 +8228,7 @@ function $RouteProvider(){ } /** - * @returns interpolation of the redirect path with the parametrs + * @returns interpolation of the redirect path with the parameters */ function interpolate(string, params) { var result = []; @@ -7676,10 +8263,6 @@ function $RouteProvider(){ * The service guarantees that the identity of the `$routeParams` object will remain unchanged * (but its properties will likely change) even when a route change occurs. * - * Note that the `$routeParams` are only updated *after* a route change completes successfully. - * This means that you cannot rely on `$routeParams` being correct in route resolve functions. - * Instead you can use `$route.current.params` to access the new route's parameters. - * * @example *
  *  // Given:
@@ -7777,25 +8360,7 @@ function $RootScopeProvider(){
      *
      * Here is a simple scope snippet to show how you can interact with the scope.
      * 
-        angular.injector(['ng']).invoke(function($rootScope) {
-           var scope = $rootScope.$new();
-           scope.salutation = 'Hello';
-           scope.name = 'World';
-
-           expect(scope.greeting).toEqual(undefined);
-
-           scope.$watch('name', function() {
-             scope.greeting = scope.salutation + ' ' + scope.name + '!';
-           }); // initialize the watch
-
-           expect(scope.greeting).toEqual(undefined);
-           scope.name = 'Misko';
-           // still old value, since watches have not been called yet
-           expect(scope.greeting).toEqual(undefined);
-
-           scope.$digest(); // fire all  the watches
-           expect(scope.greeting).toEqual('Hello Misko!');
-        });
+     * 
      * 
* * # Inheritance @@ -7891,7 +8456,6 @@ function $RootScopeProvider(){ child['this'] = child; child.$$listeners = {}; child.$parent = this; - child.$$asyncQueue = []; child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; child.$$prevSibling = this.$$childTail; if (this.$$childHead) { @@ -7995,6 +8559,14 @@ function $RootScopeProvider(){ watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } + if (typeof watchExp == 'string' && get.constant) { + var originalFn = watcher.fn; + watcher.fn = function(newVal, oldVal, scope) { + originalFn.call(this, newVal, oldVal, scope); + arrayRemove(array, watcher); + }; + } + if (!array) { array = scope.$$watchers = []; } @@ -8007,6 +8579,147 @@ function $RootScopeProvider(){ }; }, + + /** + * @ngdoc function + * @name ng.$rootScope.Scope#$watchCollection + * @methodOf ng.$rootScope.Scope + * @function + * + * @description + * Shallow watches the properties of an object and fires whenever any of the properties change + * (for arrays this implies watching the array items, for object maps this implies watching the properties). + * If a change is detected the `listener` callback is fired. + * + * - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to + * see if any items have been added, removed, or moved. + * - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items + * into the object or array, removing and moving items around. + * + * + * # Example + *
+          $scope.names = ['igor', 'matias', 'misko', 'james'];
+          $scope.dataCount = 4;
+
+          $scope.$watchCollection('names', function(newNames, oldNames) {
+            $scope.dataCount = newNames.length;
+          });
+
+          expect($scope.dataCount).toEqual(4);
+          $scope.$digest();
+
+          //still at 4 ... no changes
+          expect($scope.dataCount).toEqual(4);
+
+          $scope.names.pop();
+          $scope.$digest();
+
+          //now there's been a change
+          expect($scope.dataCount).toEqual(3);
+       * 
+ * + * + * @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The expression value + * should evaluate to an object or an array which is observed on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the collection will trigger + * a call to the `listener`. + * + * @param {function(newCollection, oldCollection, scope)} listener a callback function that is fired with both + * the `newCollection` and `oldCollection` as parameters. + * The `newCollection` object is the newly modified data obtained from the `obj` expression and the + * `oldCollection` object is a copy of the former collection data. + * The `scope` refers to the current scope. + * + * @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed + * then the internal watch operation is terminated. + */ + $watchCollection: function(obj, listener) { + var self = this; + var oldValue; + var newValue; + var changeDetected = 0; + var objGetter = $parse(obj); + var internalArray = []; + var internalObject = {}; + var oldLength = 0; + + function $watchCollectionWatch() { + newValue = objGetter(self); + var newLength, key; + + if (!isObject(newValue)) { + if (oldValue !== newValue) { + oldValue = newValue; + changeDetected++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; + } + + newLength = newValue.length; + + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; i++) { + if (oldValue[i] !== newValue[i]) { + changeDetected++; + oldValue[i] = newValue[i]; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (newValue.hasOwnProperty(key)) { + newLength++; + if (oldValue.hasOwnProperty(key)) { + if (oldValue[key] !== newValue[key]) { + changeDetected++; + oldValue[key] = newValue[key]; + } + } else { + oldLength++; + oldValue[key] = newValue[key]; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for(key in oldValue) { + if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + oldLength--; + delete oldValue[key]; + } + } + } + } + return changeDetected; + } + + function $watchCollectionAction() { + listener(newValue, oldValue, self); + } + + return this.$watch($watchCollectionWatch, $watchCollectionAction); + }, + /** * @ngdoc function * @name ng.$rootScope.Scope#$digest @@ -8058,7 +8771,7 @@ function $RootScopeProvider(){ $digest: function() { var watch, value, last, watchers, - asyncQueue, + asyncQueue = this.$$asyncQueue, length, dirty, ttl = TTL, next, current, target = this, @@ -8067,18 +8780,19 @@ function $RootScopeProvider(){ beginPhase('$digest'); - do { + do { // "while dirty" loop dirty = false; current = target; - do { - asyncQueue = current.$$asyncQueue; - while(asyncQueue.length) { - try { - current.$eval(asyncQueue.shift()); - } catch (e) { - $exceptionHandler(e); - } + + while(asyncQueue.length) { + try { + current.$eval(asyncQueue.shift()); + } catch (e) { + $exceptionHandler(e); } + } + + do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; @@ -8087,7 +8801,7 @@ function $RootScopeProvider(){ watch = watchers[length]; // 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 && (value = watch.get(current)) !== (last = watch.last) && + if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value == 'number' && typeof last == 'number' @@ -8140,9 +8854,6 @@ function $RootScopeProvider(){ * * @description * Broadcasted when a scope and its children are being destroyed. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. */ /** @@ -8164,9 +8875,6 @@ function $RootScopeProvider(){ * Just before a scope is destroyed a `$destroy` event is broadcasted on this scope. * Application code can register a `$destroy` event handler that will give it chance to * perform any necessary cleanup. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. */ $destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed @@ -8428,7 +9136,7 @@ function $RootScopeProvider(){ * Afterwards, the event propagates to all direct and indirect scopes of the current scope and * calls all registered listeners along the way. The event cannot be canceled. * - * Any exception emmited from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. * * @param {string} name Event name to broadcast. @@ -8521,17 +9229,40 @@ function $RootScopeProvider(){ * * @name ng.$sniffer * @requires $window + * @requires $document * * @property {boolean} history Does the browser support html5 history api ? * @property {boolean} hashchange Does the browser support hashchange event ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? * * @description * This is very simple implementation of testing browser's features. */ function $SnifferProvider() { - this.$get = ['$window', function($window) { + this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, - android = int((/android (\d+)/.exec(lowercase($window.navigator.userAgent)) || [])[1]); + android = int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + document = $document[0] || {}, + vendorPrefix, + vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false, + match; + + if (bodyStyle) { + for(var prop in bodyStyle) { + if(match = vendorRegex.exec(prop)) { + vendorPrefix = match[0]; + vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); + break; + } + } + transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); + animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); + } + return { // Android has history.pushState, but it does not update location correctly @@ -8541,7 +9272,7 @@ function $SnifferProvider() { history: !!($window.history && $window.history.pushState && !(android < 4)), hashchange: 'onhashchange' in $window && // IE8 compatible mode lies - (!$window.document.documentMode || $window.document.documentMode > 7), + (!document.documentMode || document.documentMode > 7), hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or @@ -8549,14 +9280,16 @@ function $SnifferProvider() { if (event == 'input' && msie == 9) return false; if (isUndefined(eventSupport[event])) { - var divElm = $window.document.createElement('div'); + var divElm = document.createElement('div'); eventSupport[event] = 'on' + event in divElm; } return eventSupport[event]; }, - // TODO(i): currently there is no way to feature detect CSP without triggering alerts - csp: false + csp: document.securityPolicy ? document.securityPolicy.isActive : false, + vendorPrefix: vendorPrefix, + transitions : transitions, + animations : animations }; }]; } @@ -8569,12 +9302,10 @@ function $SnifferProvider() { * A reference to the browser's `window` object. While `window` * is globally available in JavaScript, it causes testability problems, because * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overriden, removed or mocked for testing. + * `$window` service, so it may be overridden, removed or mocked for testing. * - * Expressions, like the one defined for the `ngClick` directive in the example - * below, are evaluated with respect to the current scope. Therefore, there is - * no risk of inadvertently coding in a dependency on a global value in such an - * expression. + * All expressions are evaluated with respect to current scope so they don't + * suffer from window globality. * * @example @@ -8632,6 +9363,43 @@ function parseHeaders(headers) { } +var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/; + + +/** + * Parse a request and location URL and determine whether this is a same-domain request. + * + * @param {string} requestUrl The url of the request. + * @param {string} locationUrl The current browser location url. + * @returns {boolean} Whether the request is for the same domain. + */ +function isSameDomain(requestUrl, locationUrl) { + var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl); + // if requestUrl is relative, the regex does not match. + if (match == null) return true; + + var domain1 = { + protocol: match[2], + host: match[4], + port: int(match[6]) || DEFAULT_PORTS[match[2]] || null, + // IE8 sets unmatched groups to '' instead of undefined. + relativeProtocol: match[2] === undefined || match[2] === '' + }; + + match = SERVER_MATCH.exec(locationUrl); + var domain2 = { + protocol: match[1], + host: match[3], + port: int(match[5]) || DEFAULT_PORTS[match[1]] || null + }; + + return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) && + domain1.host == domain2.host && + (domain1.port == domain2.port || (domain1.relativeProtocol && + domain2.port == DEFAULT_PORTS[domain2.protocol])); +} + + /** * Returns a function that provides access to parsed headers. * @@ -8689,9 +9457,10 @@ function isSuccess(status) { function $HttpProvider() { var JSON_START = /^\s*(\[|\{[^\{])/, JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/; + PROTECTION_PREFIX = /^\)\]\}',?\n/, + CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - var $config = this.defaults = { + var defaults = this.defaults = { // transform incoming response data transformResponse: [function(data) { if (isString(data)) { @@ -8711,28 +9480,63 @@ function $HttpProvider() { // default headers headers: { common: { - 'Accept': 'application/json, text/plain, */*', - 'X-Requested-With': 'XMLHttpRequest' + 'Accept': 'application/json, text/plain, */*' }, - post: {'Content-Type': 'application/json;charset=utf-8'}, - put: {'Content-Type': 'application/json;charset=utf-8'} - } + post: CONTENT_TYPE_APPLICATION_JSON, + put: CONTENT_TYPE_APPLICATION_JSON, + patch: CONTENT_TYPE_APPLICATION_JSON + }, + + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN' }; - var providerResponseInterceptors = this.responseInterceptors = []; + /** + * Are order by request. I.E. they are applied in the same order as + * array on request, but revers order on response. + */ + var interceptorFactories = this.interceptors = []; + /** + * For historical reasons, response interceptors ordered by the order in which + * they are applied to response. (This is in revers to interceptorFactories) + */ + var responseInterceptorFactories = this.responseInterceptors = []; this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { - var defaultCache = $cacheFactory('$http'), - responseInterceptors = []; + var defaultCache = $cacheFactory('$http'); - forEach(providerResponseInterceptors, function(interceptor) { - responseInterceptors.push( - isString(interceptor) - ? $injector.get(interceptor) - : $injector.invoke(interceptor) - ); + /** + * Interceptors stored in reverse order. Inner interceptors before outer interceptors. + * The reversal is needed so that we can build up the interception chain around the + * server request. + */ + var reversedInterceptors = []; + + forEach(interceptorFactories, function(interceptorFactory) { + reversedInterceptors.unshift(isString(interceptorFactory) + ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); + }); + + forEach(responseInterceptorFactories, function(interceptorFactory, index) { + var responseFn = isString(interceptorFactory) + ? $injector.get(interceptorFactory) + : $injector.invoke(interceptorFactory); + + /** + * Response interceptors go before "around" interceptors (no real reason, just + * had to pick one.) But they are already revesed, so we can't use unshift, hence + * the splice. + */ + reversedInterceptors.splice(index, 0, { + response: function(response) { + return responseFn($q.when(response)); + }, + responseError: function(response) { + return responseFn($q.reject(response)); + } + }); }); @@ -8763,7 +9567,7 @@ function $HttpProvider() { * * * # General usage - * The `$http` service is a function which takes a single argument — a configuration object — + * 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`. * @@ -8780,7 +9584,7 @@ function $HttpProvider() { *
* * 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 – + * 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. * @@ -8818,7 +9622,6 @@ function $HttpProvider() { * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): * - `Accept: application/json, text/plain, * / *` - * - `X-Requested-With: XMLHttpRequest` * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -8871,8 +9674,94 @@ function $HttpProvider() { * 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. * + * A custom default cache built with $cacheFactory can be provided in $http.defaults.cache. + * To skip it, set configuration property `cache` to `false`. * - * # Response interceptors + * + * # Interceptors + * + * Before you start creating interceptors, be sure to understand the + * {@link ng.$q $q and deferred/promise APIs}. + * + * For purposes of global error handling, authentication, or any kind of synchronous or + * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be + * able to intercept requests before they are handed to the server and + * responses before they are handed over to the application code that + * initiated these requests. The interceptors leverage the {@link ng.$q + * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. + * + * The interceptors are service factories that are registered with the `$httpProvider` by + * adding them to the `$httpProvider.interceptors` array. The factory is called and + * injected with dependencies (if specified) and returns the interceptor. + * + * There are two kinds of interceptors (and two kinds of rejection interceptors): + * + * * `request`: interceptors get called with http `config` object. The function is free to modify + * the `config` or create a new one. The function needs to return the `config` directly or as a + * promise. + * * `requestError`: interceptor gets called when a previous interceptor threw an error or resolved + * with a rejection. + * * `response`: interceptors get called with http `response` object. The function is free to modify + * the `response` or create a new one. The function needs to return the `response` directly or as a + * promise. + * * `responseError`: interceptor gets called when a previous interceptor threw an error or resolved + * with a rejection. + * + * + *
+     *   // register the interceptor as a service
+     *   $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
+     *     return {
+     *       // optional method
+     *       'request': function(config) {
+     *         // do something on success
+     *         return config || $q.when(config);
+     *       },
+     *
+     *       // optional method
+     *      'requestError': function(rejection) {
+     *         // do something on error
+     *         if (canRecover(rejection)) {
+     *           return responseOrNewPromise
+     *         }
+     *         return $q.reject(rejection);
+     *       },
+     *
+     *
+     *
+     *       // optional method
+     *       'response': function(response) {
+     *         // do something on success
+     *         return response || $q.when(response);
+     *       },
+     *
+     *       // optional method
+     *      'responseError': function(rejection) {
+     *         // do something on error
+     *         if (canRecover(rejection)) {
+     *           return responseOrNewPromise
+     *         }
+     *         return $q.reject(rejection);
+     *       };
+     *     }
+     *   });
+     *
+     *   $httpProvider.interceptors.push('myHttpInterceptor');
+     *
+     *
+     *   // register the interceptor via an anonymous factory
+     *   $httpProvider.interceptors.push(function($q, dependency1, dependency2) {
+     *     return {
+     *      'request': function(config) {
+     *          // same as above
+     *       },
+     *       'response': function(response) {
+     *          // same as above
+     *       }
+     *   });
+     * 
+ * + * # Response interceptors (DEPRECATED) * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -8885,7 +9774,7 @@ function $HttpProvider() { * * The interceptors are service factories that are registered with the $httpProvider by * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that + * injected with dependencies (if specified) and returns the interceptor — a function that * takes a {@link ng.$q promise} and returns the original or a new promise. * *
@@ -8894,7 +9783,6 @@ function $HttpProvider() {
      *     return function(promise) {
      *       return promise.then(function(response) {
      *         // do something on success
-     *         return response;
      *       }, function(response) {
      *         // do something on error
      *         if (canRecover(response)) {
@@ -8956,9 +9844,10 @@ function $HttpProvider() {
      * {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} 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
-     * called `XSRF-TOKEN` and sets it as the 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.
+     * (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
@@ -8968,30 +9857,38 @@ function $HttpProvider() {
      * up its own tokens). We recommend that the token is a digest of your site's authentication
      * cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} for added security.
      *
+     * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
+     * properties of either $httpProvider.defaults, or the per-request config object.
+     *
      *
      * @param {object} config Object describing the request to be made and how it should be
      *    processed. The object has following properties:
      *
-     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
-     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
-     *    - **params** – `{Object.}` – Map of strings or objects which will be turned to
+     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
+     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
+     *    - **params** – `{Object.}` – Map of strings or objects which will be turned to
      *      `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified.
-     *    - **data** – `{string|Object}` – Data to be sent as the request message data.
-     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server.
-     *    - **transformRequest** – `{function(data, headersGetter)|Array.}` –
+     *    - **data** – `{string|Object}` – Data to be sent as the request message data.
+     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server.
+     *    - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
+     *    - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
+     *    - **transformRequest** – `{function(data, headersGetter)|Array.}` –
      *      transform function or an array of such functions. The transform function takes the http
      *      request body and headers and returns its transformed (typically serialized) version.
-     *    - **transformResponse** – `{function(data, headersGetter)|Array.}` –
+     *    - **transformResponse** – `{function(data, headersGetter)|Array.}` –
      *      transform function or an array of such functions. The transform function takes the http
      *      response body and headers and returns its transformed (typically deserialized) version.
-     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
+     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
      *      GET request, otherwise if a cache instance built with
      *      {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
      *      caching.
-     *    - **timeout** – `{number}` – timeout in milliseconds.
+     *    - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
+     *      that should abort the request when resolved.
      *    - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the
      *      XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
      *      requests with credentials} for more information.
+     *    - **responseType** - `{string}` - see {@link
+     *      https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
      *
      * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the
      *   standard `then` method and two http specific methods: `success` and `error`. The `then`
@@ -9001,10 +9898,10 @@ function $HttpProvider() {
      *   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.
+     *   - **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.
      *
      * @property {Array.} pendingRequests Array of config objects for currently pending
      *   requests. This is primarily meant to be used for debugging purposes.
@@ -9081,57 +9978,66 @@ function $HttpProvider() {
         
       
      */
-    function $http(config) {
+    function $http(requestConfig) {
+      var config = {
+        transformRequest: defaults.transformRequest,
+        transformResponse: defaults.transformResponse
+      };
+      var headers = {};
+
+      extend(config, requestConfig);
+      config.headers = headers;
       config.method = uppercase(config.method);
 
-      var reqTransformFn = config.transformRequest || $config.transformRequest,
-          respTransformFn = config.transformResponse || $config.transformResponse,
-          reqHeaders = extend({}, config.headers),
-          defHeaders = extend(
-            {'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
-            $config.headers.common,
-            $config.headers[lowercase(config.method)]
-          ),
-          reqData,
-          defHeaderName, lowercaseDefHeaderName, headerName,
-          promise;
+      extend(headers,
+          defaults.headers.common,
+          defaults.headers[lowercase(config.method)],
+          requestConfig.headers);
 
-      // using for-in instead of forEach to avoid unecessary iteration after header has been found
-      defaultHeadersIteration:
-      for(defHeaderName in defHeaders) {
-        lowercaseDefHeaderName = lowercase(defHeaderName);
-        for(headerName in config.headers) {
-          if (lowercase(headerName) === lowercaseDefHeaderName) {
-            continue defaultHeadersIteration;
-          }
-        }
-        reqHeaders[defHeaderName] = defHeaders[defHeaderName];
+      var xsrfValue = isSameDomain(config.url, $browser.url())
+          ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
+          : undefined;
+      if (xsrfValue) {
+        headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
       }
 
-      // strip content-type if data is undefined
-      if (isUndefined(config.data)) {
-        for(var header in reqHeaders) {
-          if (lowercase(header) === 'content-type') {
-            delete reqHeaders[header];
-            break;
-          }
+
+      var serverRequest = function(config) {
+        var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
+
+        // strip content-type if data is undefined
+        if (isUndefined(config.data)) {
+          delete headers['Content-Type'];
         }
-      }
 
-      reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn);
+        if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
+          config.withCredentials = defaults.withCredentials;
+        }
 
-      // send request
-      promise = sendReq(config, reqData, reqHeaders);
+        // send request
+        return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
+      };
 
-
-      // transform future response
-      promise = promise.then(transformResponse, transformResponse);
+      var chain = [serverRequest, undefined];
+      var promise = $q.when(config);
 
       // apply interceptors
-      forEach(responseInterceptors, function(interceptor) {
-        promise = interceptor(promise);
+      forEach(reversedInterceptors, function(interceptor) {
+        if (interceptor.request || interceptor.requestError) {
+          chain.unshift(interceptor.request, interceptor.requestError);
+        }
+        if (interceptor.response || interceptor.responseError) {
+          chain.push(interceptor.response, interceptor.responseError);
+        }
       });
 
+      while(chain.length) {
+        var thenFn = chain.shift();
+        var rejectFn = chain.shift();
+
+        promise = promise.then(thenFn, rejectFn);
+      }
+
       promise.success = function(fn) {
         promise.then(function(response) {
           fn(response.data, response.status, response.headers, config);
@@ -9151,7 +10057,7 @@ function $HttpProvider() {
       function transformResponse(response) {
         // make a copy since the response must be cacheable
         var resp = extend({}, response, {
-          data: transformData(response.data, response.headers, respTransformFn)
+          data: transformData(response.data, response.headers, config.transformResponse)
         });
         return (isSuccess(response.status))
           ? resp
@@ -9251,11 +10157,11 @@ function $HttpProvider() {
          *
          * @description
          * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of
-         * default headers as well as request and response transformations.
+         * default headers, withCredentials as well as request and response transformations.
          *
          * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above.
          */
-    $http.defaults = $config;
+    $http.defaults = defaults;
 
 
     return $http;
@@ -9290,7 +10196,7 @@ function $HttpProvider() {
      * Makes the request.
      *
      * !!! ACCESSES CLOSURE VARS:
-     * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests
+     * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
      */
     function sendReq(config, reqData, reqHeaders) {
       var deferred = $q.defer(),
@@ -9303,8 +10209,10 @@ function $HttpProvider() {
       promise.then(removePendingReq, removePendingReq);
 
 
-      if (config.cache && config.method == 'GET') {
-        cache = isObject(config.cache) ? config.cache : defaultCache;
+      if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') {
+        cache = isObject(config.cache) ? config.cache
+              : isObject(defaults.cache) ? defaults.cache
+              : defaultCache;
       }
 
       if (cache) {
@@ -9331,7 +10239,7 @@ function $HttpProvider() {
       // if we won't have the response in cache, send the request to the backend
       if (!cachedResp) {
         $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
-            config.withCredentials);
+            config.withCredentials, config.responseType);
       }
 
       return promise;
@@ -9354,7 +10262,7 @@ function $HttpProvider() {
         }
 
         resolvePromise(response, status, headersString);
-        $rootScope.$apply();
+        if (!$rootScope.$$phase) $rootScope.$apply();
       }
 
 
@@ -9386,10 +10294,15 @@ function $HttpProvider() {
           var parts = [];
           forEachSorted(params, function(value, key) {
             if (value == null || value == undefined) return;
-            if (isObject(value)) {
-              value = toJson(value);
-            }
-            parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
+            if (!isArray(value)) value = [value];
+
+            forEach(value, function(v) {
+              if (isObject(v)) {
+                v = toJson(v);
+              }
+              parts.push(encodeUriQuery(key) + '=' +
+                         encodeUriQuery(v));
+            });
           });
           return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
         }
@@ -9432,7 +10345,8 @@ function $HttpBackendProvider() {
 
 function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) {
   // TODO(vojta): fix the signature
-  return function(method, url, post, callback, headers, timeout, withCredentials) {
+  return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
+    var status;
     $browser.$$incOutstandingRequestCount();
     url = url || $browser.url();
 
@@ -9442,12 +10356,12 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
         callbacks[callbackId].data = data;
       };
 
-      jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
+      var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
           function() {
         if (callbacks[callbackId].data) {
           completeRequest(callback, 200, callbacks[callbackId].data);
         } else {
-          completeRequest(callback, -2);
+          completeRequest(callback, status || -2);
         }
         delete callbacks[callbackId];
       });
@@ -9458,8 +10372,6 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
         if (value) xhr.setRequestHeader(key, value);
       });
 
-      var status;
-
       // In IE6 and 7, this might be called synchronously when xhr.send below is called and the
       // response is in the cache. the promise api will ensure that to the app code the api is
       // always async
@@ -9487,8 +10399,12 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
           }
           // end of the workaround.
 
-          completeRequest(callback, status || xhr.status, xhr.responseText,
-                          responseHeaders);
+          // responseText is the old-school way of retrieving response (supported by IE8 & 9)
+          // response and responseType properties were introduced in XHR Level2 spec (supported by IE10)
+          completeRequest(callback,
+              status || xhr.status,
+              (xhr.responseType ? xhr.response : xhr.responseText),
+              responseHeaders);
         }
       };
 
@@ -9496,20 +10412,33 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
         xhr.withCredentials = true;
       }
 
-      xhr.send(post || '');
-
-      if (timeout > 0) {
-        $browserDefer(function() {
-          status = -1;
-          xhr.abort();
-        }, timeout);
+      if (responseType) {
+        xhr.responseType = responseType;
       }
+
+      xhr.send(post || '');
+    }
+
+    if (timeout > 0) {
+      var timeoutId = $browserDefer(timeoutRequest, timeout);
+    } else if (timeout && timeout.then) {
+      timeout.then(timeoutRequest);
     }
 
 
+    function timeoutRequest() {
+      status = -1;
+      jsonpDone && jsonpDone();
+      xhr && xhr.abort();
+    }
+
     function completeRequest(callback, status, response, headersString) {
       // URL_MATCH is defined in src/service/location.js
-      var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1];
+      var protocol = (url.match(SERVER_MATCH) || ['', locationProtocol])[1];
+
+      // cancel timeout and subsequent timeout promise resolution
+      timeoutId && $browserDefer.cancel(timeoutId);
+      jsonpDone = xhr = null;
 
       // fix status code for file protocol (it's always 0)
       status = (protocol == 'file') ? (response ? 200 : 404) : status;
@@ -9544,6 +10473,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
     }
 
     rawDocument.body.appendChild(script);
+    return doneWrapper;
   }
 }
 
@@ -9555,7 +10485,7 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument,
  * $locale service provides localization rules for various Angular components. As of right now the
  * only public api is:
  *
- * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
+ * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
  */
 function $LocaleProvider(){
   this.$get = function() {
@@ -9662,15 +10592,17 @@ function $TimeoutProvider() {
           deferred.reject(e);
           $exceptionHandler(e);
         }
-        finally {
-          delete deferreds[promise.$$timeoutId];
-        }
 
         if (!skipApply) $rootScope.$apply();
       }, delay);
 
+      cleanup = function() {
+        delete deferreds[promise.$$timeoutId];
+      };
+
       promise.$$timeoutId = timeoutId;
       deferreds[timeoutId] = deferred;
+      promise.then(cleanup, cleanup);
 
       return promise;
     }
@@ -9692,7 +10624,6 @@ function $TimeoutProvider() {
     timeout.cancel = function(promise) {
       if (promise && promise.$$timeoutId in deferreds) {
         deferreds[promise.$$timeoutId].reject('canceled');
-        delete deferreds[promise.$$timeoutId];
         return $browser.defer.cancel(promise.$$timeoutId);
       }
       return false;
@@ -9836,6 +10767,22 @@ function $FilterProvider($provide) {
  *     called for each element of `array`. The final result is an array of those elements that
  *     the predicate returned true for.
  *
+ * @param {function(expected, actual)|true|undefined} comparator Comparator which is used in
+ *     determining if the expected value (from the filter expression) and actual value (from
+ *     the object in the array) should be considered a match.
+ *
+ *   Can be one of:
+ *
+ *     - `function(expected, actual)`:
+ *       The function will be given the object value and the predicate value to compare and
+ *       should return true if the item should be included in filtered result.
+ *
+ *     - `true`: A shorthand for `function(expected, actual) { return angular.equals(expected, actual)}`.
+ *       this is essentially strict comparison of expected and actual.
+ *
+ *     - `false|undefined`: A short hand for a function which will look for a substring match in case
+ *       insensitive way.
+ *
  * @example
    
      
@@ -9843,7 +10790,8 @@ function $FilterProvider($provide) {
                                 {name:'Mary', phone:'800-BIG-MARY'},
                                 {name:'Mike', phone:'555-4321'},
                                 {name:'Adam', phone:'555-5678'},
-                                {name:'Julie', phone:'555-8765'}]">
+                                {name:'Julie', phone:'555-8765'},
+                                {name:'Juliette', phone:'555-5678'}]">
 
        Search: 
        
@@ -9857,9 +10805,10 @@ function $FilterProvider($provide) {
        Any: 
Name only
Phone only
+ Equality
- + @@ -9879,13 +10828,19 @@ function $FilterProvider($provide) { it('should search in specific fields when filtering with a predicate object', function() { input('search.$').enter('i'); expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Mike', 'Julie']); + toEqual(['Mary', 'Mike', 'Julie', 'Juliette']); + }); + it('should use a equal comparison when comparator is true', function() { + input('search.name').enter('Julie'); + input('strict').check(); + expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')). + toEqual(['Julie']); }); */ function filterFilter() { - return function(array, expression) { + return function(array, expression, comperator) { if (!isArray(array)) return array; var predicates = []; predicates.check = function(value) { @@ -9896,20 +10851,43 @@ function filterFilter() { } return true; }; + switch(typeof comperator) { + case "function": + break; + case "boolean": + if(comperator == true) { + comperator = function(obj, text) { + return angular.equals(obj, text); + } + break; + } + default: + comperator = function(obj, text) { + text = (''+text).toLowerCase(); + return (''+obj).toLowerCase().indexOf(text) > -1 + }; + } var search = function(obj, text){ - if (text.charAt(0) === '!') { + if (typeof text == 'string' && text.charAt(0) === '!') { return !search(obj, text.substr(1)); } switch (typeof obj) { case "boolean": case "number": case "string": - return ('' + obj).toLowerCase().indexOf(text) > -1; + return comperator(obj, text); case "object": - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } + switch (typeof text) { + case "object": + return comperator(obj, text); + break; + default: + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + break; } return false; case "array": @@ -9932,19 +10910,18 @@ function filterFilter() { for (var key in expression) { if (key == '$') { (function() { - var text = (''+expression[key]).toLowerCase(); - if (!text) return; + if (!expression[key]) return; + var path = key predicates.push(function(value) { - return search(value, text); + return search(value, expression[path]); }); })(); } else { (function() { + if (!expression[key]) return; var path = key; - var text = (''+expression[key]).toLowerCase(); - if (!text) return; predicates.push(function(value) { - return search(getter(value, path), text); + return search(getter(value,path), expression[path]); }); })(); } @@ -10029,10 +11006,8 @@ function currencyFilter($locale) { * If the input is not a number an empty string is returned. * * @param {number|string} number Number to format. - * @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 decimalPlaces and places a “,” after each third digit. + * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example @@ -10138,11 +11113,6 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); - } else { - - if (fractionSize > 0 && number > -1 && number < 1) { - formatedText = number.toFixed(fractionSize); - } } parts.push(isNegative ? pattern.negPre : pattern.posPre); @@ -10217,6 +11187,9 @@ var DATE_FORMATS = { m: dateGetter('Minutes', 1), ss: dateGetter('Seconds', 2), s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, @@ -10255,6 +11228,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) + * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) * * `'a'`: am/pm marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) * @@ -10266,7 +11240,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) @@ -10274,7 +11248,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `format` string can contain literal values. These need to be quoted with single quotes (e.g. * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h 'o''clock'"`). + * (e.g. `"h o''clock"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its @@ -10311,18 +11285,26 @@ function dateFilter($locale) { var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - function jsonStringToDate(string){ + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { var match; if (match = string.match(R_ISO8601_STR)) { var date = new Date(0), tzHour = 0, - tzMin = 0; + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; + if (match[9]) { tzHour = int(match[9] + match[10]); tzMin = int(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); + dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); + var h = int(match[4]||0) - tzHour; + var m = int(match[5]||0) - tzMin + var s = int(match[6]||0); + var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + timeSetter.call(date, h, m, s, ms); return date; } return string; @@ -10436,20 +11418,20 @@ var uppercaseFilter = valueFn(uppercase); * @function * * @description - * Creates a new array containing only a specified number of elements in an array. The elements - * are taken from either the beginning or the end of the source array, as specified by the - * value and sign (positive or negative) of `limit`. + * Creates a new array or string containing only a specified number of elements. The elements + * are taken from either the beginning or the end of the source array or string, as specified by + * the value and sign (positive or negative) of `limit`. * * Note: This function is used to augment the `Array` type in Angular expressions. See * {@link ng.$filter} for more information about Angular arrays. * - * @param {Array} array Source array to be limited. - * @param {string|Number} limit The length of the returned array. If the `limit` number is - * positive, `limit` number of items from the beginning of the source array are copied. - * If the number is negative, `limit` number of items from the end of the source array are - * copied. The `limit` will be trimmed if it exceeds `array.length` - * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit` - * elements. + * @param {Array|string} input Source array or string to be limited. + * @param {string|number} limit The length of the returned array or string. If the `limit` number + * is positive, `limit` number of items from the beginning of the source array/string are copied. + * If the number is negative, `limit` number of items from the end of the source array/string + * are copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array + * had less than `limit` elements. * * @example @@ -10457,59 +11439,76 @@ var uppercaseFilter = valueFn(uppercase);
- Limit {{numbers}} to: -

Output: {{ numbers | limitTo:limit }}

+ Limit {{numbers}} to: +

Output numbers: {{ numbers | limitTo:numLimit }}

+ Limit {{letters}} to: +

Output letters: {{ letters | limitTo:letterLimit }}

- it('should limit the numer array to first three items', function() { - expect(element('.doc-example-live input[ng-model=limit]').val()).toBe('3'); - expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3]'); + it('should limit the number array to first three items', function() { + expect(element('.doc-example-live input[ng-model=numLimit]').val()).toBe('3'); + expect(element('.doc-example-live input[ng-model=letterLimit]').val()).toBe('3'); + expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3]'); + expect(binding('letters | limitTo:letterLimit')).toEqual('abc'); }); it('should update the output when -3 is entered', function() { - input('limit').enter(-3); - expect(binding('numbers | limitTo:limit')).toEqual('[7,8,9]'); + input('numLimit').enter(-3); + input('letterLimit').enter(-3); + expect(binding('numbers | limitTo:numLimit')).toEqual('[7,8,9]'); + expect(binding('letters | limitTo:letterLimit')).toEqual('ghi'); }); it('should not exceed the maximum size of input array', function() { - input('limit').enter(100); - expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3,4,5,6,7,8,9]'); + input('numLimit').enter(100); + input('letterLimit').enter(100); + expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3,4,5,6,7,8,9]'); + expect(binding('letters | limitTo:letterLimit')).toEqual('abcdefghi'); });
*/ function limitToFilter(){ - return function(array, limit) { - if (!(array instanceof Array)) return array; + return function(input, limit) { + if (!isArray(input) && !isString(input)) return input; + limit = int(limit); + + if (isString(input)) { + //NaN check on limit + if (limit) { + return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); + } else { + return ""; + } + } + var out = [], i, n; - // check that array is iterable - if (!array || !(array instanceof Array)) - return out; - // if abs(limit) exceeds maximum length, trim it - if (limit > array.length) - limit = array.length; - else if (limit < -array.length) - limit = -array.length; + if (limit > input.length) + limit = input.length; + else if (limit < -input.length) + limit = -input.length; if (limit > 0) { i = 0; n = limit; } else { - i = array.length + limit; - n = array.length; + i = input.length + limit; + n = input.length; } for (; i} expression A predicate to be @@ -10641,10 +11640,8 @@ function orderByFilter($parse){ var t1 = typeof v1; var t2 = typeof v2; if (t1 == t2) { - if (t1 == "string") { - v1 = v1.toLowerCase(); - v2 = v2.toLowerCase(); - } + if (t1 == "string") v1 = v1.toLowerCase(); + if (t1 == "string") v2 = v2.toLowerCase(); if (v1 === v2) return 0; return v1 < v2 ? -1 : 1; } else { @@ -10812,6 +11809,31 @@ var htmlAnchorDirective = valueFn({ * @param {template} ngSrc any string which can contain `{{}}` markup. */ +/** + * @ngdoc directive + * @name ng.directive:ngSrcset + * @restrict A + * + * @description + * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until Angular replaces the expression inside + * `{{hash}}`. The `ngSrcset` directive solves this problem. + * + * The buggy way to write it: + *
+ * 
+ * 
+ * + * The correct way to write it: + *
+ * 
+ * 
+ * + * @element IMG + * @param {template} ngSrcset any string which can contain `{{}}` markup. + */ + /** * @ngdoc directive * @name ng.directive:ngDisabled @@ -10979,6 +12001,37 @@ var htmlAnchorDirective = valueFn({ * @param {string} expression Angular expression that will be evaluated. */ +/** + * @ngdoc directive + * @name ng.directive:ngOpen + * @restrict A + * + * @description + * The HTML specs do not require browsers to preserve the special attributes such as open. + * (The presence of them means true and absence means false) + * This prevents the angular compiler from correctly retrieving the binding expression. + * To solve this problem, we introduce the `ngOpen` directive. + * + * @example + + + Check me check multiple:
+
+ Show/Hide me +
+
+ + it('should toggle open', function() { + expect(element('#details').prop('open')).toBeFalsy(); + input('open').check(); + expect(element('#details').prop('open')).toBeTruthy(); + }); + +
+ * + * @element DETAILS + * @param {string} expression Angular expression that will be evaluated. + */ var ngAttributeAliasDirectives = {}; @@ -11001,8 +12054,8 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }); -// ng-src, ng-href are interpolated -forEach(['src', 'href'], function(attrName) { +// ng-src, ng-srcset, ng-href are interpolated +forEach(['src', 'srcset', 'href'], function(attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { @@ -11029,7 +12082,8 @@ var nullFormCtrl = { $addControl: noop, $removeControl: noop, $setValidity: noop, - $setDirty: noop + $setDirty: noop, + $setPristine: noop }; /** @@ -11044,7 +12098,7 @@ var nullFormCtrl = { * @property {Object} $error Is an object hash, containing references to all invalid controls or * forms, where: * - * - keys are validation tokens (error names) — such as `required`, `url` or `email`), + * - keys are validation tokens (error names) — such as `required`, `url` or `email`), * - values are arrays of controls or forms that are invalid with given error. * * @description @@ -11061,10 +12115,11 @@ function FormController(element, attrs) { var form = this, parentForm = element.parent().controller('form') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}; + errors = form.$error = {}, + controls = []; // init state - form.$name = attrs.name || attrs.ngForm; + form.$name = attrs.name; form.$dirty = false; form.$pristine = true; form.$valid = true; @@ -11084,32 +12139,14 @@ function FormController(element, attrs) { addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); } - /** - * @ngdoc function - * @name ng.directive:form.FormController#$addControl - * @methodOf ng.directive:form.FormController - * - * @description - * Register a control with the form. - * - * Input elements using ngModelController do this automatically when they are linked. - */ form.$addControl = function(control) { + controls.push(control); + if (control.$name && !form.hasOwnProperty(control.$name)) { form[control.$name] = control; } }; - /** - * @ngdoc function - * @name ng.directive:form.FormController#$removeControl - * @methodOf ng.directive:form.FormController - * - * @description - * Deregister a control from the form. - * - * Input elements using ngModelController do this automatically when they are destroyed. - */ form.$removeControl = function(control) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; @@ -11117,18 +12154,10 @@ function FormController(element, attrs) { forEach(errors, function(queue, validationToken) { form.$setValidity(validationToken, true, control); }); + + arrayRemove(controls, control); }; - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setValidity - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ form.$setValidity = function(validationToken, isValid, control) { var queue = errors[validationToken]; @@ -11167,17 +12196,6 @@ function FormController(element, attrs) { } }; - /** - * @ngdoc function - * @name ng.directive:form.FormController#$setDirty - * @methodOf ng.directive:form.FormController - * - * @description - * Sets the form to a dirty state. - * - * This method can be called to add the 'ng-dirty' class and set the form to a dirty - * state (ng-dirty class). This method will also propagate to parent forms. - */ form.$setDirty = function() { element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); form.$dirty = true; @@ -11185,6 +12203,29 @@ function FormController(element, attrs) { parentForm.$setDirty(); }; + /** + * @ngdoc function + * @name ng.directive:form.FormController#$setPristine + * @methodOf ng.directive:form.FormController + * + * @description + * Sets the form to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the form to its pristine + * state (ng-pristine class). This method will also propagate to all the controls contained + * in this form. + * + * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after + * saving or resetting it. + */ + form.$setPristine = function () { + element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); + form.$dirty = false; + form.$pristine = true; + forEach(controls, function(control) { + control.$setPristine(); + }); + }; } @@ -11354,7 +12395,7 @@ var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/; +var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var inputType = { @@ -11381,6 +12422,8 @@ var inputType = { * patterns defined as scope expressions. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trimming the + * input. * * @example @@ -11388,12 +12431,12 @@ var inputType = {
Single word: + ng-pattern="word" required ng-trim="false"> Required! @@ -11422,6 +12465,12 @@ var inputType = { input('text').enter('hello world'); expect(binding('myForm.input.$valid')).toEqual('false'); }); + + it('should not be trimmed', function() { + input('text').enter('untrimmed '); + expect(binding('text')).toEqual('untrimmed '); + expect(binding('myForm.input.$valid')).toEqual('true'); + });
*/ @@ -11465,9 +12514,9 @@ var inputType = { Number: - + Required! - + Not valid number! value = {{value}}
myForm.input.$valid = {{myForm.input.$valid}}
@@ -11588,8 +12637,6 @@ var inputType = { * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. * * @example @@ -11737,7 +12784,14 @@ function isEmpty(value) { function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { var listener = function() { - var value = trim(element.val()); + var value = element.val(); + + // By default we will trim the value + // If the attribute ng-trim exists we will avoid trimming + // e.g. + if (toBoolean(attr.ngTrim || 'T')) { + value = trim(value); + } if (ctrl.$viewValue !== value) { scope.$apply(function() { @@ -11788,7 +12842,8 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // pattern validator var pattern = attr.ngPattern, - patternValidator; + patternValidator, + match; var validate = function(regexp, value) { if (isEmpty(value) || regexp.test(value)) { @@ -11801,8 +12856,9 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }; if (pattern) { - if (pattern.match(/^\/(.*)\/$/)) { - pattern = new RegExp(pattern.substr(1, pattern.length - 2)); + match = pattern.match(/^\/(.*)\/([gim]*)$/); + if (match) { + pattern = new RegExp(match[1], match[2]); patternValidator = function(value) { return validate(pattern, value) }; @@ -12148,26 +13204,13 @@ var VALID_CLASS = 'ng-valid', * * @property {string} $viewValue Actual string value in the view. * @property {*} $modelValue The value in the model, that the control is bound to. - * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. Each function is called, in turn, passing the value - through to the next. Used to sanitize / convert the value as well as validation. - - For validation, the parsers should update the validity state using - {@link ng.directive:ngModel.NgModelController#$setValidity $setValidity()}, - and return `undefined` for invalid values. + * @property {Array.} $parsers Whenever the control reads value from the DOM, it executes + * all of these functions to sanitize / convert the value as well as validate. * - * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - * the model value changes. Each function is called, in turn, passing the value through to the - * next. Used to format / convert values for display in the control and validation. - *
- *      function formatter(value) {
- *        if (value) {
- *          return value.toUpperCase();
- *        }
- *      }
- *      ngModel.$formatters.push(formatter);
- *      
- * @property {Object} $error An bject hash with all errors as keys. + * @property {Array.} $formatters Whenever the model value changes, it executes all of + * these functions to convert the value as well as validate. + * + * @property {Object} $error An object hash with all errors as keys. * * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. @@ -12181,10 +13224,6 @@ var VALID_CLASS = 'ng-valid', * specifically does not contain any logic which deals with DOM rendering or listening to * DOM events. The `NgModelController` is meant to be extended by other directives where, the * directive provides DOM manipulation and the `NgModelController` provides the data-binding. - * Note that you cannot use `NgModelController` in a directive with an isolated scope, - * as, in that case, the `ng-model` value gets put into the isolated scope and does not get - * propogated to the parent scope. - * * * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) @@ -12225,13 +13264,7 @@ var VALID_CLASS = 'ng-valid', // Write data to the model function read() { - var html = element.html(); - // When we clear the content editable the browser leaves a
behind - // If strip-br attribute is provided then we strip this out - if( attrs.stripBr && html == '
' ) { - html = ''; - } - ngModel.$setViewValue(html); + ngModel.$setViewValue(element.html()); } } }; @@ -12241,7 +13274,6 @@ var VALID_CLASS = 'ng-valid',
Change me!
Required!
@@ -12351,6 +13383,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ parentForm.$setValidity(validationErrorKey, isValid, this); }; + /** + * @ngdoc function + * @name ng.directive:ngModel.NgModelController#$setPristine + * @methodOf ng.directive:ngModel.NgModelController + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the 'ng-dirty' class and set the control to its pristine + * state (ng-pristine class). + */ + this.$setPristine = function () { + this.$dirty = false; + this.$pristine = true; + $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); + }; /** * @ngdoc function @@ -12364,8 +13412,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * For example {@link ng.directive:input input} or * {@link ng.directive:select select} directives call it. * - * It internally calls all `$parsers` (including validators) and updates the `$modelValue` and the actual model path. - * Lastly it calls all registered change listeners. + * It internally calls all `parsers` and if resulted value is valid, updates the model and + * calls all registered change listeners. * * @param {string} value Value from the view. */ @@ -12430,7 +13478,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @element input * * @description - * Is a directive that tells Angular to do two-way data binding. It works together with `input`, + * Is directive that tells Angular to do two-way data binding. It works together with `input`, * `select`, `textarea`. You can easily write your own directives to use `ngModel` as well. * * `ngModel` is responsible for: @@ -12442,10 +13490,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`), * - register the control with parent {@link ng.directive:form form}. * - * Note: `ngModel` will try to bind to the property given by evaluating the expression on the - * current scope. If the property doesn't already exist on this scope, it will be created - * implicitly and added to the scope. - * * For basic examples, how to use `ngModel`, see: * * - {@link ng.directive:input input} @@ -12586,9 +13630,8 @@ var requiredDirective = function() { List: - + Required! -
names = {{names}}
myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
myForm.namesInput.$error = {{myForm.namesInput.$error}}
@@ -12600,14 +13643,12 @@ var requiredDirective = function() { it('should initialize to model', function() { expect(binding('names')).toEqual('["igor","misko","vojta"]'); expect(binding('myForm.namesInput.$valid')).toEqual('true'); - expect(element('span.error').css('display')).toBe('none'); }); it('should be invalid if empty', function() { input('names').enter(''); expect(binding('names')).toEqual('[]'); expect(binding('myForm.namesInput.$valid')).toEqual('false'); - expect(element('span.error').css('display')).not().toBe('none'); });
@@ -12657,7 +13698,7 @@ var ngValueDirective = function() { } else { return function(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); + attr.$set('value', value, false); }); }; } @@ -12677,9 +13718,10 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferrable to use `ngBind` instead of `{{ expression }}` when a template is momentarily - * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an - * element attribute, it makes the bindings invisible to the user while the page is loading. + * One scenario in which the use of `ngBind` is preferred over `{{ expression }}` binding is when + * it's desirable to put bindings into template that is momentarily displayed by the browser in its + * raw state before Angular compiles it. Since `ngBind` is an element attribute, it makes the + * bindings invisible to the user while the page is loading. * * An alternative solution to this problem would be using the * {@link ng.directive:ngCloak ngCloak} directive. @@ -12725,11 +13767,10 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { * * @description * The `ngBindTemplate` directive specifies that the element - * text content should be replaced with the interpolation of the template - * in the `ngBindTemplate` attribute. - * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` - * expressions. This directive is needed since some HTML elements - * (such as TITLE and OPTION) cannot contain SPAN elements. + * text should be replaced with the template in ngBindTemplate. + * Unlike ngBind the ngBindTemplate can contain multiple `{{` `}}` + * expressions. (This is required since some HTML elements + * can not have SPAN elements such as TITLE, or OPTION to name a few.) * * @element ANY * @param {string} ngBindTemplate template of form @@ -12865,8 +13906,8 @@ function classDirective(name, selector) { * @name ng.directive:ngClass * * @description - * The `ngClass` allows you to set CSS classes on HTML an element, dynamically, by databinding - * an expression that represents all classes to be added. + * The `ngClass` allows you to set CSS class on HTML element dynamically by databinding an + * expression that represents all classes to be added. * * The directive won't add duplicate classes if a particular class was already set. * @@ -12876,9 +13917,7 @@ function classDirective(name, selector) { * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result * of the evaluation can be a string representing space delimited class - * names, an array, or a map of class names to boolean values. In the case of a map, the - * names of the properties whose values are truthy will be added as css classes to the - * element. + * names, an array, or a map of class names to boolean values. * * @example @@ -13017,14 +14056,14 @@ var ngClassEvenDirective = classDirective('Even', 1); * directive to avoid the undesirable flicker effect caused by the html template display. * * The directive can be applied to the `` element, but typically a fine-grained application is - * prefered in order to benefit from progressive rendering of the browser view. + * preferred in order to benefit from progressive rendering of the browser view. * * `ngCloak` works in cooperation with a css rule that is embedded within `angular.js` and * `angular.min.js` files. Following is the css rule: * *
  * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- *   display: none !important;
+ *   display: none;
  * }
  * 
* @@ -13077,9 +14116,9 @@ var ngCloakDirective = ngDirective({ * * MVC components in angular: * - * * Model — The Model is data in scope properties; scopes are attached to the DOM. - * * View — The template (HTML with data bindings) is rendered into the View. - * * Controller — The `ngController` directive specifies a Controller class; the class has + * * Model — The Model is data in scope properties; scopes are attached to the DOM. + * * View — The template (HTML with data bindings) is rendered into the View. + * * Controller — The `ngController` directive specifies a Controller class; the class has * methods that typically express the business logic behind the application. * * Note that an alternative way to define controllers is via the {@link ng.$route $route} service. @@ -13088,14 +14127,84 @@ var ngCloakDirective = ngDirective({ * @scope * @param {expression} ngController Name of a globally accessible constructor function or an * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. + * constructor function. The controller instance can further be published into the scope + * by adding `as localName` the controller name attribute. * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and - * greeting are methods declared on the $scope by the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that any changes to the data are automatically - * reflected in the View without the need for a manual update. + * greeting are methods declared on the controller (see source tab). These methods can + * easily be called from the angular markup. Notice that the scope becomes the `this` for the + * controller's instance. This allows for easy access to the view data from the controller. Also + * notice that any changes to the data are automatically reflected in the View without the need + * for a manual update. The example is included in two different declaration styles based on + * your style preferences. + + +
+ Name: + [ greet ]
+ Contact: +
    +
  • + + + [ clear + | X ] +
  • +
  • [ add ]
  • +
+
+
+ + it('should check controller', function() { + expect(element('.doc-example-live div>:input').val()).toBe('John Smith'); + expect(element('.doc-example-live li:nth-child(1) input').val()) + .toBe('408 555 1212'); + expect(element('.doc-example-live li:nth-child(2) input').val()) + .toBe('john.smith@example.org'); + + element('.doc-example-live li:first a:contains("clear")').click(); + expect(element('.doc-example-live li:first input').val()).toBe(''); + + element('.doc-example-live li:last a:contains("add")').click(); + expect(element('.doc-example-live li:nth-child(3) input').val()) + .toBe('yourname@example.org'); + }); + +
+ + + + -
- - selection={{selection}} -
-
+ + +
+ + selection={{selection}} +
+
Settings Div
- Home Span - default -
+
Home Span
+
default
- - - it('should start in settings', function() { - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Settings Div/); - }); - it('should change to home', function() { - select('selection').option('home'); - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Home Span/); - }); - it('should select deafault', function() { - select('selection').option('other'); - expect(element('.doc-example-live [ng-switch]').text()).toMatch(/default/); - }); - - - */ -var NG_SWITCH = 'ng-switch'; -var ngSwitchDirective = valueFn({ - restrict: 'EA', - require: 'ngSwitch', - // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { - this.cases = {}; - }], - link: function(scope, element, attr, ctrl) { - var watchExpr = attr.ngSwitch || attr.on, - selectedTransclude, - selectedElement, - selectedScope; +
+ + + function Ctrl($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + } + + + .example-leave, .example-enter { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + -o-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; - scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - if (selectedElement) { - selectedScope.$destroy(); - selectedElement.remove(); - selectedElement = selectedScope = null; + position:absolute; + top:0; + left:0; + right:0; + bottom:0; } - if ((selectedTransclude = ctrl.cases['!' + value] || ctrl.cases['?'])) { - scope.$eval(attr.change); - selectedScope = scope.$new(); - selectedTransclude(selectedScope, function(caseElement) { - selectedElement = caseElement; - element.append(caseElement); - }); + + .example-animate-container > * { + display:block; + padding:10px; } - }); + + .example-enter { + top:-50px; + } + .example-enter.example-enter-active { + top:0; + } + + .example-leave { + top:0; + } + .example-leave.example-leave-active { + top:50px; + } + + + it('should start in settings', function() { + expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Settings Div/); + }); + it('should change to home', function() { + select('selection').option('home'); + expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Home Span/); + }); + it('should select default', function() { + select('selection').option('other'); + expect(element('.doc-example-live [ng-switch]').text()).toMatch(/default/); + }); + + + */ +var ngSwitchDirective = ['$animator', function($animator) { + return { + restrict: 'EA', + require: 'ngSwitch', + + // asks for $scope to fool the BC controller module + controller: ['$scope', function ngSwitchController() { + this.cases = {}; + }], + link: function(scope, element, attr, ngSwitchController) { + var animate = $animator(scope, attr); + var watchExpr = attr.ngSwitch || attr.on, + selectedTranscludes, + selectedElements, + selectedScopes = []; + + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { + for (var i= 0, ii=selectedScopes.length; i' + '
{{title}}
' + '
' + @@ -14372,11 +15993,18 @@ var ngTranscludeDirective = ngDirective({ * Every time the current route changes, the included view changes with it according to the * configuration of the `$route` service. * + * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** + * and **leave** effects. + * + * @animations + * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM) + * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM + * * @scope * @example - + -
+
Choose: Moby | Moby: Ch1 | @@ -14384,57 +16012,106 @@ var ngTranscludeDirective = ngDirective({ Gatsby: Ch4 | Scarlet Letter
-
+

-
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
+
$location.path() = {{main.$location.path()}}
+
$route.current.templateUrl = {{main.$route.current.templateUrl}}
+
$route.current.params = {{main.$route.current.params}}
+
$route.current.scope.name = {{main.$route.current.scope.name}}
+
$routeParams = {{main.$routeParams}}
- controller: {{name}}
- Book Id: {{params.bookId}}
+
+ controller: {{book.name}}
+ Book Id: {{book.params.bookId}}
+
- controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}} +
+ controller: {{chapter.name}}
+ Book Id: {{chapter.params.bookId}}
+ Chapter Id: {{chapter.params.chapterId}} +
+
+ + + .example-leave, .example-enter { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + } + + .example-animate-container { + position:relative; + height:100px; + } + + .example-animate-container > * { + display:block; + width:100%; + border-left:1px solid black; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + padding:10px; + } + + .example-enter { + left:100%; + } + .example-enter.example-enter-active { + left:0; + } + + .example-leave { } + .example-leave.example-leave-active { + left:-100%; + } angular.module('ngView', [], function($routeProvider, $locationProvider) { $routeProvider.when('/Book/:bookId', { templateUrl: 'book.html', - controller: BookCntl + controller: BookCntl, + controllerAs: 'book' }); $routeProvider.when('/Book/:bookId/ch/:chapterId', { templateUrl: 'chapter.html', - controller: ChapterCntl + controller: ChapterCntl, + controllerAs: 'chapter' }); // configure html5 to get links working on jsfiddle $locationProvider.html5Mode(true); }); - function MainCntl($scope, $route, $routeParams, $location) { - $scope.$route = $route; - $scope.$location = $location; - $scope.$routeParams = $routeParams; + function MainCntl($route, $routeParams, $location) { + this.$route = $route; + this.$location = $location; + this.$routeParams = $routeParams; } - function BookCntl($scope, $routeParams) { - $scope.name = "BookCntl"; - $scope.params = $routeParams; + function BookCntl($routeParams) { + this.name = "BookCntl"; + this.params = $routeParams; } - function ChapterCntl($scope, $routeParams) { - $scope.name = "ChapterCntl"; - $scope.params = $routeParams; + function ChapterCntl($routeParams) { + this.name = "ChapterCntl"; + this.params = $routeParams; } @@ -14465,15 +16142,16 @@ var ngTranscludeDirective = ngDirective({ * Emitted every time the ngView content is reloaded. */ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', - '$controller', + '$controller', '$animator', function($http, $templateCache, $route, $anchorScroll, $compile, - $controller) { + $controller, $animator) { return { restrict: 'ECA', terminal: true, link: function(scope, element, attr) { var lastScope, - onloadExp = attr.onload || ''; + onloadExp = attr.onload || '', + animate = $animator(scope, attr); scope.$on('$routeChangeSuccess', update); update(); @@ -14487,7 +16165,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c } function clearContent() { - element.html(''); + animate.leave(element.contents(), element); destroyLastScope(); } @@ -14496,10 +16174,11 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c template = locals && locals.$template; if (template) { - element.html(template); - destroyLastScope(); + clearContent(); + var enterElements = jqLite('
').html(template).contents(); + animate.enter(enterElements, element); - var link = $compile(element.contents()), + var link = $compile(enterElements), current = $route.current, controller; @@ -14507,6 +16186,9 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c if (current.controller) { locals.$scope = lastScope; controller = $controller(current.controller, locals); + if (current.controllerAs) { + lastScope[current.controllerAs] = controller; + } element.children().data('$ngControllerController', controller); } @@ -14582,8 +16264,8 @@ var scriptDirective = ['$templateCache', function($templateCache) { * Optionally `ngOptions` attribute can be used to dynamically generate a list of `` * DOM element. + * * `trackexpr`: Used when working with an array of objects. The result of this expression will be + * used to identify the objects in the array. The `trackexpr` will most likely refer to the + * `value` variable (e.g. `value.propertyName`). * * @example @@ -14692,8 +16377,8 @@ var scriptDirective = ['$templateCache', function($templateCache) { var ngOptionsDirective = valueFn({ terminal: true }); var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770 - var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/, + //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000007777000000000000000000088888 + var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/, nullModelCtrl = {$setViewValue: noop}; return { @@ -14867,7 +16552,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { throw Error( - "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_ (track by _expr_)?'" + " but got '" + optionsExp + "'."); } @@ -14877,6 +16562,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { groupByFn = $parse(match[3] || ''), valueFn = $parse(match[2] ? match[1] : valueName), valuesFn = $parse(match[7]), + track = match[8], + trackFn = track ? $parse(match[8]) : null, // This is an array of array of existing option groups in DOM. We try to reuse these if possible // optionGroupsCache[0] is the options with no option group // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element @@ -14917,7 +16604,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { if ((optionElement = optionGroup[index].element)[0].selected) { key = optionElement.val(); if (keyName) locals[keyName] = key; - locals[valueName] = collection[key]; + if (trackFn) { + for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) break; + } + } else { + locals[valueName] = collection[key]; + } value.push(valueFn(scope, locals)); } } @@ -14929,9 +16623,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } else if (key == ''){ value = null; } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); + if (trackFn) { + for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) { + locals[valueName] = collection[trackIndex]; + if (trackFn(scope, locals) == key) { + value = valueFn(scope, locals); + break; + } + } + } else { + locals[valueName] = collection[key]; + if (keyName) locals[keyName] = key; + value = valueFn(scope, locals); + } } } ctrl.$setViewValue(value); @@ -14963,7 +16667,15 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { label; if (multiple) { - selectedSet = new HashMap(modelValue); + if (trackFn && isArray(modelValue)) { + selectedSet = new HashMap([]); + for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { + locals[valueName] = modelValue[trackIndex]; + selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + } + } else { + selectedSet = new HashMap(modelValue); + } } // We now build up the list of options we need (we merge later) @@ -14975,15 +16687,21 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { optionGroupNames.push(optionGroupName); } if (multiple) { - selected = selectedSet.remove(valueFn(scope, locals)) != undefined; + selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) != undefined; } else { - selected = modelValue === valueFn(scope, locals); + if (trackFn) { + var modelCast = {}; + modelCast[valueName] = modelValue; + selected = trackFn(scope, modelCast) === trackFn(scope, locals); + } else { + selected = modelValue === valueFn(scope, locals); + } selectedSet = selectedSet || selected; // see if at least one item is selected } label = displayFn(scope, locals); // what will be seen by the user label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values optionGroup.push({ - id: keyName ? keys[index] : index, // either the index into array or key from object + id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), // either the index into array or key from object label: label, selected: selected // determine if we should be selected }); @@ -15155,4 +16873,4 @@ var styleDirective = valueFn({ }); })(window, document); -angular.element(document).find('head').append(''); \ No newline at end of file +angular.element(document).find('head').append(''); \ No newline at end of file
NamePhone
{{friend.name}} {{friend.phone}}