From 4972f53cf38e0390543dd5c7dd3d1fa17291bbfd Mon Sep 17 00:00:00 2001 From: bullgare Date: Fri, 3 Oct 2014 14:02:00 +0400 Subject: [PATCH 01/14] Fixes bug when $location.search() is not returning search part of current url. --- src/ng/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index 5e425ab0d65a..55a0c5a6b9ac 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -480,7 +480,7 @@ LocationHashbangInHtml5Url.prototype = if (value == null) delete search[key]; }); - this.$$search = search; + this.$$search = extend({}, search); } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); From e7cf04bad33bd86ce5508069abff7023bff40097 Mon Sep 17 00:00:00 2001 From: Dominic Watson Date: Tue, 27 May 2014 11:47:27 +0200 Subject: [PATCH 02/14] docs(angular.element): `css()` method does not retrieve computed styles The jQuery css() getter functionality utilises getComputedStyle() whereas jqLite only retrieves what is declared inline on an element. Closes #7599 --- src/jqLite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jqLite.js b/src/jqLite.js index 67b0602320ce..b199f390e329 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -44,7 +44,7 @@ * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyles()` * - [`data()`](http://api.jquery.com/data/) * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) From d277641eec6a9074aaf248e2e434cbfce5f45bf3 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Fri, 3 Oct 2014 13:33:04 -0700 Subject: [PATCH 03/14] docs(guide/accessibility): Update documentation --- docs/content/guide/accessibility.ngdoc | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/content/guide/accessibility.ngdoc b/docs/content/guide/accessibility.ngdoc index 85a094c4d10e..098cdf6ea688 100644 --- a/docs/content/guide/accessibility.ngdoc +++ b/docs/content/guide/accessibility.ngdoc @@ -6,8 +6,26 @@ # Accessibility with ngAria -You can use the `ngAria` module to have certain ARIA attributes automatically applied when you -use certain directives. +You can use the `ngAria` module to have common ARIA attributes automatically applied when you +use certain directives. To enable `ngAria`, just require the module into your application and +the code will hook into your ng-show/ng-hide, input, textarea, button, select and +ng-required directives and add the appropriate ARIA states and properties. + +Currently, the following attributes are implemented: + * aria-hidden + * aria-checked + * aria-disabled + * aria-required + * aria-invalid + * aria-multiline + * aria-valuenow + * aria-valuemin + * aria-valuemax + * tabindex + +You can disable individual attributes by using the `{@link ngAria.$ariaProvider#config config}` method. + +###Example ```js angular.module('myApp', ['ngAria'])... @@ -39,3 +57,4 @@ Accessibility best practices that apply to web apps in general also apply to Ang * [WebAim](http://webaim.org/) * [Using WAI-ARIA in HTML](http://www.w3.org/TR/2014/WD-aria-in-html-20140626/) +* [Apps For All: Coding Accessible Web Applications](https://shop.smashingmagazine.com/apps-for-all-coding-accessible-web-applications.html) From 404b95fe30a1bcd1313adafbd0018578d5b21d3d Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Tue, 23 Sep 2014 18:00:37 -0700 Subject: [PATCH 04/14] fix($browser): handle async href on url change in <=IE9 Closes #9235 --- src/ng/browser.js | 6 +++++- test/ng/browserSpecs.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index e83d34a69768..e97592d5d750 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -182,6 +182,10 @@ function Browser(window, document, $log, $sniffer) { function fireUrlChange() { newLocation = null; + checkUrlChange(); + } + + function checkUrlChange() { if (lastBrowserUrl == self.url()) return; lastBrowserUrl = self.url(); @@ -237,7 +241,7 @@ function Browser(window, document, $log, $sniffer) { * Needs to be exported to be able to check for changes that have been done in sync, * as hashchange/popstate events fire in async. */ - self.$$checkUrlChange = fireUrlChange; + self.$$checkUrlChange = checkUrlChange; ////////////////////////////////////////////////////////////// // Misc API diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index f5a4359625c2..401e40597975 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -646,6 +646,29 @@ describe('browser', function() { expect($location.path()).toBe('/someTestHash'); }); }); + + }); + + describe('integration test with $rootScope', function() { + + beforeEach(module(function($provide, $locationProvider) { + $provide.value('$browser', browser); + browser.pollFns = []; + })); + + it('should not interfere with legacy browser url replace behavior', function() { + inject(function($rootScope) { + var current = fakeWindow.location.href; + var newUrl = 'notyet'; + sniffer.history = false; + browser.url(https://github.com/newUrl,%20true); + expect(browser.url()).toBe(newUrl); + $rootScope.$digest(); + expect(browser.url()).toBe(newUrl); + expect(fakeWindow.location.href).toBe(current); + }); + }); + }); }); From f3539f3cb5d9477f50f065c6a0ac7d6ca0a31092 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 3 Oct 2014 09:46:42 -0700 Subject: [PATCH 05/14] fix($compile): use the correct namespace for transcluded svg elements This fixes the case when a directive that uses `templateUrl` is used somewhere in the children of a transcluding directive like `ng-repeat`. Fixes #9344 Related to #8808 Closes #9415 --- src/ng/compile.js | 27 ++++++++++++++------------- test/ng/compileSpec.js | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 9cb0a75e9ef4..55ffcc006153 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1150,27 +1150,28 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; - var namespaceAdaptedCompileNodes = $compileNodes; - var lastCompileNode; return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ assertArg(scope, 'scope'); if (!namespace) { namespace = detectNamespaceForChildElements(futureParentElement); } - if (namespace !== 'html' && $compileNodes[0] !== lastCompileNode) { - namespaceAdaptedCompileNodes = jqLite( + var $linkNode; + if (namespace !== 'html') { + // When using a directive with replace:true and templateUrl the $compileNodes + // (or a child element inside of them) + // might change, so we need to recreate the namespace adapted compileNodes + // for call to the link function. + // Note: This will already clone the nodes... + $linkNode = jqLite( wrapTemplate(namespace, jqLite('
').append($compileNodes).html()) ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; } - // When using a directive with replace:true and templateUrl the $compileNodes - // might change, so we need to recreate the namespace adapted compileNodes. - lastCompileNode = $compileNodes[0]; - - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call(namespaceAdaptedCompileNodes) // IMPORTANT!!! - : namespaceAdaptedCompileNodes; if (transcludeControllers) { for (var controllerName in transcludeControllers) { diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 6b425cca4c1e..bf47683d96fd 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -300,7 +300,7 @@ describe('$compile', function() { }); inject(function($compile, $rootScope, $httpBackend) { $httpBackend.expect('GET', 'template.html').respond(''); - element = $compile('')($rootScope); + element = $compile('')($rootScope); // initially the template is not yet loaded $rootScope.$apply(function() { From 0656484d3e709c5162570b0dd6473b0b6140e5b2 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 2 Oct 2014 17:55:32 -0700 Subject: [PATCH 06/14] =?UTF-8?q?fix($browser):=20don=E2=80=99t=20use=20hi?= =?UTF-8?q?story=20api=20when=20only=20the=20hash=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IE10/11 have the following problem: When changing the url hash via `history.pushState()` and then reverting the hash via direct changes to `location.href` (or via a link) does not fire a `hashchange` nor `popstate` event. This commit changes the default behavior as follows: Uses `location.href`/`location.replace` if the new url differs from the previous url only in the hash fragment or the browser does not support history API. Use `history.pushState`/ `history.replaceState` otherwise. Fixes #9143 Closes #9406 --- src/ng/browser.js | 7 ++++++- src/ng/location.js | 3 +++ test/ng/browserSpecs.js | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index e97592d5d750..583468eaa7b6 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -153,8 +153,13 @@ function Browser(window, document, $log, $sniffer) { // setter if (url) { if (lastBrowserUrl == url) return; + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if (!sameBase && $sniffer.history) { if (replace) history.replaceState(null, '', url); else { history.pushState(null, '', url); diff --git a/src/ng/location.js b/src/ng/location.js index 5e425ab0d65a..f7275319dcb9 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -720,6 +720,9 @@ function $LocationProvider(){ if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. event.preventDefault(); // update location manually if ($location.absUrl() != $browser.url()) { diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 401e40597975..86d2db7a77bb 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -445,6 +445,17 @@ describe('browser', function() { expect(locationReplace).not.toHaveBeenCalled(); }); + it('should set location.href and not use pushState when the url only changed in the hash fragment to please IE10/11', function() { + sniffer.history = true; + browser.url('http://server/#123'); + + expect(fakeWindow.location.href).toEqual('http://server/#123'); + + expect(pushState).not.toHaveBeenCalled(); + expect(replaceState).not.toHaveBeenCalled(); + expect(locationReplace).not.toHaveBeenCalled(); + }); + it('should use location.replace when history.replaceState not available', function() { sniffer.history = false; browser.url('http://new.org/', true); @@ -456,6 +467,17 @@ describe('browser', function() { expect(fakeWindow.location.href).toEqual('http://server/'); }); + it('should use location.replace and not use replaceState when the url only changed in the hash fragment to please IE10/11', function() { + sniffer.history = true; + browser.url('http://server/#123',%20true); + + expect(locationReplace).toHaveBeenCalledWith('http://server/#123'); + + expect(pushState).not.toHaveBeenCalled(); + expect(replaceState).not.toHaveBeenCalled(); + expect(fakeWindow.location.href).toEqual('http://server/'); + }); + it('should return $browser to allow chaining', function() { expect(browser.url('http://any.com/')).toBe(browser); }); From f7174169f4f710d605f6a67f39f90a67a07d4cab Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Thu, 4 Sep 2014 23:07:39 +0300 Subject: [PATCH 07/14] fix(select): use $viewValue instead of $modelValue Closes #8929 --- src/ng/directive/select.js | 20 +++--- test/ng/directive/selectSpec.js | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 10 deletions(-) diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index d1f275a1ba6c..46d38717c37b 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -436,16 +436,16 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { function getSelectedSet() { var selectedSet = false; if (multiple) { - var modelValue = ctrl.$modelValue; - if (trackFn && isArray(modelValue)) { + var viewValue = ctrl.$viewValue; + if (trackFn && isArray(viewValue)) { selectedSet = new HashMap([]); var locals = {}; - for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { - locals[valueName] = modelValue[trackIndex]; - selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); + for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) { + locals[valueName] = viewValue[trackIndex]; + selectedSet.put(trackFn(scope, locals), viewValue[trackIndex]); } } else { - selectedSet = new HashMap(modelValue); + selectedSet = new HashMap(viewValue); } } return selectedSet; @@ -470,7 +470,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { optionGroup, option, existingParent, existingOptions, existingOption, - modelValue = ctrl.$modelValue, + viewValue = ctrl.$viewValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, key, @@ -508,10 +508,10 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } else { if (trackFn) { var modelCast = {}; - modelCast[valueName] = modelValue; + modelCast[valueName] = viewValue; selected = trackFn(scope, modelCast) === trackFn(scope, locals); } else { - selected = modelValue === valueFn(scope, locals); + selected = viewValue === valueFn(scope, locals); } selectedSet = selectedSet || selected; // see if at least one item is selected } @@ -527,7 +527,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }); } if (!multiple) { - if (nullOption || modelValue === null) { + if (nullOption || viewValue === null) { // insert null option if we have a placeholder, or the model is null optionGroups[''].unshift({id:'', label:'', selected:!selectedSet}); } else if (!selectedSet) { diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js index 3a93fb50304e..fb423d516622 100644 --- a/test/ng/directive/selectSpec.js +++ b/test/ng/directive/selectSpec.js @@ -1550,6 +1550,130 @@ describe('select', function() { expect(scope.value).toBe(false); }); }); + + describe('ngModelCtrl', function() { + it('should prefix the model value with the word "the" using $parsers', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']', + }); + + scope.form.select.$parsers.push(function(value) { + return 'the ' + value; + }); + + element.val('2'); + browserTrigger(element, 'change'); + expect(scope.value).toBe('the third'); + expect(element.val()).toBe('2'); + }); + + it('should prefix the view value with the word "the" using $formatters', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'the first\', \'the second\', \'the third\', \'the fourth\']', + }); + + scope.form.select.$formatters.push(function(value) { + return 'the ' + value; + }); + + scope.$apply(function() { + scope.value = 'third'; + }); + expect(element.val()).toBe('2'); + }); + + it('should fail validation when $validators fail', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']', + }); + + scope.form.select.$validators.fail = function() { + return false; + }; + + element.val('2'); + browserTrigger(element, 'change'); + expect(element).toBeInvalid(); + expect(scope.value).toBeUndefined(); + expect(element.val()).toBe('2'); + }); + + it('should pass validation when $validators pass', function() { + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']', + }); + + scope.form.select.$validators.pass = function() { + return true; + }; + + element.val('2'); + browserTrigger(element, 'change'); + expect(element).toBeValid(); + expect(scope.value).toBe('third'); + expect(element.val()).toBe('2'); + }); + + it('should fail validation when $asyncValidators fail', inject(function($q, $rootScope) { + var defer; + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']', + }); + + scope.form.select.$asyncValidators.async = function() { + defer = $q.defer(); + return defer.promise; + }; + + element.val('2'); + browserTrigger(element, 'change'); + expect(scope.form.select.$pending).toBeDefined(); + expect(scope.value).toBeUndefined(); + expect(element.val()).toBe('2'); + + defer.reject(); + $rootScope.$digest(); + expect(scope.form.select.$pending).toBeUndefined(); + expect(scope.value).toBeUndefined(); + expect(element.val()).toBe('2'); + })); + + it('should pass validation when $asyncValidators pass', inject(function($q, $rootScope) { + var defer; + createSelect({ + 'name': 'select', + 'ng-model': 'value', + 'ng-options': 'item for item in [\'first\', \'second\', \'third\', \'fourth\']', + }); + + scope.form.select.$asyncValidators.async = function() { + defer = $q.defer(); + return defer.promise; + }; + + element.val('2'); + browserTrigger(element, 'change'); + expect(scope.form.select.$pending).toBeDefined(); + expect(scope.value).toBeUndefined(); + expect(element.val()).toBe('2'); + + defer.resolve(); + $rootScope.$digest(); + expect(scope.form.select.$pending).toBeUndefined(); + expect(scope.value).toBe('third'); + expect(element.val()).toBe('2'); + })); + }); }); From 858360b680a2bb5c19429c1be1c9506700cda476 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 3 Oct 2014 16:23:09 -0700 Subject: [PATCH 08/14] =?UTF-8?q?fix($browser):=20don=E2=80=99t=20use=20hi?= =?UTF-8?q?story=20api=20when=20only=20the=20hash=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix jshint error --- src/ng/browser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ng/browser.js b/src/ng/browser.js index 583468eaa7b6..e78c90ffd37b 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -1,4 +1,5 @@ 'use strict'; +/* global stripHash: true */ /** * ! This is a private undocumented service ! From 7b6c1d08aceba6704a40302f373400aed9ed0e0b Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Thu, 2 Oct 2014 13:46:22 -0700 Subject: [PATCH 09/14] fix($http): honor application/json response header and parse json primitives When server responds with Content-Type header set to application/json we now properly parse the response as JSON Closes #2973 --- src/ng/http.js | 10 ++++++--- test/ng/httpSpec.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index e10207c57f99..272a8d5faadf 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -89,7 +89,8 @@ function $HttpProvider() { var JSON_START = /^\s*(\[|\{[^\{])/, JSON_END = /[\}\]]\s*$/, PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; + APPLICATION_JSON = 'application/json', + CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; /** * @ngdoc property @@ -114,12 +115,15 @@ function $HttpProvider() { **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { + transformResponse: [function defaultHttpResponseTransform(data, headers) { if (isString(data)) { // strip json vulnerability protection prefix data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) + var contentType = headers('Content-Type'); + if ((contentType && contentType.indexOf(APPLICATION_JSON) === 0) || + (JSON_START.test(data) && JSON_END.test(data))) { data = fromJson(data); + } } return data; }], diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index c26c84334eca..f76aaec81371 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1020,6 +1020,61 @@ describe('$http', function() { }); + it('should deserialize json numbers when response header contains application/json', + function() { + $httpBackend.expect('GET', '/url').respond('123', {'Content-Type': 'application/json'}); + $http({method: 'GET', url: '/url'}).success(callback); + $httpBackend.flush(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual(123); + }); + + + it('should deserialize json strings when response header contains application/json', + function() { + $httpBackend.expect('GET', '/url').respond('"asdf"', {'Content-Type': 'application/json'}); + $http({method: 'GET', url: '/url'}).success(callback); + $httpBackend.flush(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual('asdf'); + }); + + + it('should deserialize json nulls when response header contains application/json', + function() { + $httpBackend.expect('GET', '/url').respond('null', {'Content-Type': 'application/json'}); + $http({method: 'GET', url: '/url'}).success(callback); + $httpBackend.flush(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual(null); + }); + + + it('should deserialize json true when response header contains application/json', + function() { + $httpBackend.expect('GET', '/url').respond('true', {'Content-Type': 'application/json'}); + $http({method: 'GET', url: '/url'}).success(callback); + $httpBackend.flush(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual(true); + }); + + + it('should deserialize json false when response header contains application/json', + function() { + $httpBackend.expect('GET', '/url').respond('false', {'Content-Type': 'application/json'}); + $http({method: 'GET', url: '/url'}).success(callback); + $httpBackend.flush(); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback.mostRecentCall.args[0]).toEqual(false); + }); + + it('should deserialize json with security prefix', function() { $httpBackend.expect('GET', '/url').respond(')]}\',\n[1, "abc", {"foo":"bar"}]'); $http({method: 'GET', url: '/url'}).success(callback); From feba0174db0f8f929273beb8b90691734a9292e2 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Mon, 22 Sep 2014 13:14:31 -0400 Subject: [PATCH 10/14] fix($compile): remove comment nodes from templates before asserting single root node The compiler will no longer throw if a directive template contains comment nodes in addition to a single root node. If a template contains less than 2 nodes, the nodes are unaltered. BREAKING CHANGE: If a template contains directives within comment nodes, and there is more than a single node in the template, those comment nodes are removed. The impact of this breaking change is expected to be quite low. Closes #9212 Closes #9215 --- src/.jshintrc | 1 + src/Angular.js | 2 ++ src/ng/compile.js | 21 +++++++++++++++++++-- test/ng/compileSpec.js | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/.jshintrc b/src/.jshintrc index b8e18287ba38..e2d6c01b7474 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -11,6 +11,7 @@ "jqLite": false, "jQuery": false, "slice": false, + "splice": false, "push": false, "toString": false, "ngMinErr": false, diff --git a/src/Angular.js b/src/Angular.js index aa549c68740f..ae2831d2ac65 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -6,6 +6,7 @@ jqLite: true, jQuery: true, slice: true, + splice: true, push: true, toString: true, ngMinErr: true, @@ -161,6 +162,7 @@ var /** holds major version number for IE or NaN for real browsers */ jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, + splice = [].splice, push = [].push, toString = Object.prototype.toString, ngMinErr = minErr('ng'), diff --git a/src/ng/compile.js b/src/ng/compile.js index 55ffcc006153..2f47798e77d7 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1623,7 +1623,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(wrapTemplate(directive.templateNamespace, trim(directiveValue))); + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; @@ -2105,7 +2105,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(wrapTemplate(templateNamespace, trim(content))); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; @@ -2519,3 +2519,20 @@ function tokenDifference(str1, str2) { } return values; } + +function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === 8) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; +} diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index bf47683d96fd..2367df30ac9f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1078,6 +1078,22 @@ describe('$compile', function() { }); }); } + + it('should ignore comment nodes when replacing with a template', function() { + module(function() { + directive('replaceWithComments', valueFn({ + replace: true, + template: '

Hello, world!

' + })); + }); + inject(function($compile, $rootScope) { + expect(function() { + element = $compile('
')($rootScope); + }).not.toThrow(); + expect(element.find('p').length).toBe(1); + expect(element.find('p').text()).toBe('Hello, world!'); + }); + }); }); @@ -1977,6 +1993,26 @@ describe('$compile', function() { }); }); } + + it('should ignore comment nodes when replacing with a templateUrl', function() { + module(function() { + directive('replaceWithComments', valueFn({ + replace: true, + templateUrl: 'templateWithComments.html' + })); + }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.whenGET('templateWithComments.html'). + respond('

Hello, world!

'); + expect(function() { + element = $compile('
')($rootScope); + }).not.toThrow(); + $httpBackend.flush(); + expect(element.find('p').length).toBe(1); + expect(element.find('p').text()).toBe('Hello, world!'); + }); + }); + }); From 7cb01a80beec669d8f6aae1dc211d2f0b7d4eac4 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 3 Oct 2014 21:22:59 -0700 Subject: [PATCH 11/14] =?UTF-8?q?fix($browser):=20don=E2=80=99t=20use=20hi?= =?UTF-8?q?story=20api=20when=20only=20the=20hash=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a failing test on IE9 caused as a side effect of 404b95fe30a1bcd1313adafbd0018578d5b21d3d being merged before 0656484d3e709c5162570b0dd6473b0b6140e5b2. The test should have been independent on the browser running it and it is now. Closes #9423 Closes #9424 --- test/ng/browserSpecs.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 86d2db7a77bb..ffd393b8dd24 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -652,6 +652,9 @@ describe('browser', function() { $provide.value('$browser', browser); browser.pollFns = []; + sniffer.history = true; + $provide.value('$sniffer', sniffer); + $locationProvider.html5Mode(true); })); From 3dd7e72dbec62c9ae824f7abd4b3cb659c981b91 Mon Sep 17 00:00:00 2001 From: bullgare Date: Fri, 3 Oct 2014 14:02:00 +0400 Subject: [PATCH 12/14] Fixes bug when $location.search() is not returning search part of current url. --- src/ng/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index f7275319dcb9..7079c8685d13 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -480,7 +480,7 @@ LocationHashbangInHtml5Url.prototype = if (value == null) delete search[key]; }); - this.$$search = search; + this.$$search = extend({}, search); } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); From 03df3f0171fc9546bfe1a883c2f92258b1f847ac Mon Sep 17 00:00:00 2001 From: bullgare Date: Mon, 6 Oct 2014 17:55:32 +0400 Subject: [PATCH 13/14] Fixes bug when $location.search() is not returning search part of current url. Tests added and implementation fixed. --- src/ng/location.js | 3 ++- test/ng/locationSpec.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index 7079c8685d13..8b5e955f5114 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -475,12 +475,13 @@ LocationHashbangInHtml5Url.prototype = search = search.toString(); this.$$search = parseKeyValue(search); } else if (isObject(search)) { + search = copy(search, {}); // remove object undefined or null properties forEach(search, function(value, key) { if (value == null) delete search[key]; }); - this.$$search = extend({}, search); + this.$$search = search; } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index d6cf2cbeb090..92a1448274b9 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -169,6 +169,16 @@ describe('$location', function() { }); + it('search() should not use given object directly', function() { + var obj = {one: 1, two: true, three: null}; + url.search(obj); + expect(obj).toEqual({one: 1, two: true, three: null}); + obj.one = 'changed'; + expect(url.search()).toEqual({one: 1, two: true}); + expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash'); + }); + + it('search() should change single parameter', function() { url.search({id: 'old', preserved: true}); url.search('id', 'new'); From 5335cd1f615b6e78d340dce5c9bb0d837db86244 Mon Sep 17 00:00:00 2001 From: bullgare Date: Mon, 6 Oct 2014 18:06:38 +0400 Subject: [PATCH 14/14] fixed merge --- src/ng/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index 02b40a83446f..8b5e955f5114 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -481,7 +481,7 @@ LocationHashbangInHtml5Url.prototype = if (value == null) delete search[key]; }); - this.$$search = extend({}, search); + this.$$search = search; } else { throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.');