diff --git a/src/bootstrap/bootstrap-prettify.js b/src/bootstrap/bootstrap-prettify.js index e7fc1e9e6a42..a4eb52f242e7 100644 --- a/src/bootstrap/bootstrap-prettify.js +++ b/src/bootstrap/bootstrap-prettify.js @@ -190,7 +190,12 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', $provide.value('$anchorScroll', angular.noop); $provide.value('$browser', $browser); $provide.provider('$location', function() { - this.$get = function() { return $location; }; + this.$get = ['$rootScope', function($rootScope) { + docsRootScope.$on('$locationChangeSuccess', function(event, oldUrl, newUrl) { + $rootScope.$broadcast('$locationChangeSuccess', oldUrl, newUrl); + }); + return $location; + }]; this.html5Mode = angular.noop; }); $provide.decorator('$defer', ['$rootScope', '$delegate', function($rootScope, $delegate) { diff --git a/src/ng/location.js b/src/ng/location.js index c7b41605e0d4..64a234ad5588 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -408,7 +408,10 @@ function locationGetterSetter(property, preprocess) { * @requires $rootElement * * @description - * The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar. + * The $location service parses the URL in the browser address bar (based on the + * {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL + * available to your application. Changes to the URL in the address bar are reflected into + * $location service and changes to $location are reflected into the browser address bar. * * **The $location service:** * @@ -421,7 +424,8 @@ function locationGetterSetter(property, preprocess) { * - Clicks on a link. * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * - * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location} + * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular + * Services: Using $location} */ /** @@ -470,65 +474,73 @@ function $LocationProvider(){ this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', function( $rootScope, $browser, $sniffer, $rootElement) { - var currentUrl, + var $location, basePath = $browser.baseHref() || '/', pathPrefix = pathPrefixFromBase(basePath), - initUrl = $browser.url(); + initUrl = $browser.url(), + absUrlPrefix; if (html5Mode) { if ($sniffer.history) { - currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix); + $location = new LocationUrl( + convertToHtml5Url(initUrl, basePath, hashPrefix), + pathPrefix); } else { - currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), - hashPrefix); + $location = new LocationHashbangUrl( + convertToHashbangUrl(initUrl, basePath, hashPrefix), + hashPrefix); } + } else { + $location = new LocationHashbangUrl(initUrl, hashPrefix); + } - // link rewriting - var u = currentUrl, - absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix; + // link rewriting + absUrlPrefix = composeProtocolHostPort( + $location.protocol(), $location.host(), $location.port()) + pathPrefix; - $rootElement.bind('click', function(event) { - // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) - // currently we open nice url link and redirect then + $rootElement.bind('click', function(event) { + // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) + // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (event.ctrlKey || event.metaKey || event.which == 2) return; - var elm = jqLite(event.target); + var elm = jqLite(event.target); - // traverse the DOM up to find first A tag - while (elm.length && lowercase(elm[0].nodeName) !== 'a') { - elm = elm.parent(); - } + // traverse the DOM up to find first A tag + while (elm.length && lowercase(elm[0].nodeName) !== 'a') { + elm = elm.parent(); + } - var absHref = elm.prop('href'); + var absHref = elm.prop('href'); - if (!absHref || - elm.attr('target') || - absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path - return; - } + if (!absHref || + elm.attr('target') || + absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path + return; + } + + // update location with href without the prefix + $location.url(absHref.substr(absUrlPrefix.length)); + $rootScope.$apply(); + event.preventDefault(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + window.angular['ff-684208-preventDefault'] = true; + }); - // update location with href without the prefix - currentUrl.url(absHref.substr(absUrlPrefix.length)); - $rootScope.$apply(); - event.preventDefault(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; - }); - } else { - currentUrl = new LocationHashbangUrl(initUrl, hashPrefix); - } // rewrite hashbang url <> html5 url - if (currentUrl.absUrl() != initUrl) { - $browser.url(currentUrl.absUrl(), true); + if ($location.absUrl() != initUrl) { + $browser.url($location.absUrl(), true); } // update $location when $browser url changes $browser.onUrlChange(function(newUrl) { - if (currentUrl.absUrl() != newUrl) { + if ($location.absUrl() != newUrl) { $rootScope.$evalAsync(function() { - currentUrl.$$parse(newUrl); + var oldUrl = $location.absUrl(); + + $location.$$parse(newUrl); + afterLocationChange(oldUrl); }); if (!$rootScope.$$phase) $rootScope.$digest(); } @@ -537,17 +549,29 @@ function $LocationProvider(){ // update browser var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - if ($browser.url() != currentUrl.absUrl()) { + var oldUrl = $browser.url(); + + if (!changeCounter || oldUrl != $location.absUrl()) { changeCounter++; $rootScope.$evalAsync(function() { - $browser.url(currentUrl.absUrl(), currentUrl.$$replace); - currentUrl.$$replace = false; + if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). + defaultPrevented) { + $location.$$parse(oldUrl); + } else { + $browser.url($location.absUrl(), $location.$$replace); + $location.$$replace = false; + afterLocationChange(oldUrl); + } }); } return changeCounter; }); - return currentUrl; + return $location; + + function afterLocationChange(oldUrl) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + } }]; } diff --git a/src/ng/route.js b/src/ng/route.js index 032e37166d62..ec05d1f8d6e4 100644 --- a/src/ng/route.js +++ b/src/ng/route.js @@ -286,7 +286,6 @@ function $RouteProvider(){ */ var matcher = switchRouteMatcher, - dirty = 0, forceReload = false, $route = { routes: routes, @@ -304,12 +303,12 @@ function $RouteProvider(){ * creates new scope, reinstantiates the controller. */ reload: function() { - dirty++; forceReload = true; + $rootScope.$evalAsync(updateRoute); } }; - $rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute); + $rootScope.$on('$locationChangeSuccess', updateRoute); return $route; diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 46079ea6b52d..8b876a3ca0cd 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -791,19 +791,6 @@ describe('$location', function() { }); - it('should not rewrite when history disabled', function() { - configureService('#new', false); - inject( - initBrowser(), - initLocation(), - function($browser) { - browserTrigger(link, 'click'); - expectNoRewrite($browser); - } - ); - }); - - it('should not rewrite full url links do different domain', function() { configureService('http://www.dot.abc/a?b=c', true); inject( @@ -982,4 +969,148 @@ describe('$location', function() { }); } }); + + + describe('location cancellation', function() { + it('should fire $before/afterLocationChange event', inject(function($location, $browser, $rootScope, $log) { + expect($browser.url()).toEqual('http://server/'); + + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + expect($location.url()).toEqual(''); + $location.url('/somePath'); + expect($location.url()).toEqual('/somePath'); + expect($browser.url()).toEqual('http://server/'); + expect($log.info.logs).toEqual([]); + + $rootScope.$apply(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/somePath', 'http://server/', 'http://server/#/somePath']); + expect($location.url()).toEqual('/somePath'); + expect($browser.url()).toEqual('http://server/#/somePath'); + })); + + + it('should allow $locationChangeStart event cancellation', inject(function($location, $browser, $rootScope, $log) { + expect($browser.url()).toEqual('http://server/'); + expect($location.url()).toEqual(''); + + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + event.preventDefault(); + }); + $rootScope.$on('$locationChangeCompleted', function(event, newUrl, oldUrl) { + throw Error('location should have been canceled'); + }); + + expect($location.url()).toEqual(''); + $location.url('/somePath'); + expect($location.url()).toEqual('/somePath'); + expect($browser.url()).toEqual('http://server/'); + expect($log.info.logs).toEqual([]); + + $rootScope.$apply(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']); + expect($log.info.logs[1]).toBeUndefined(); + expect($location.url()).toEqual(''); + expect($browser.url()).toEqual('http://server/'); + })); + + it ('should fire $locationChangeCompleted event when change from browser location bar', + inject(function($log, $location, $browser, $rootScope) { + $rootScope.$apply(); // clear initial $locationChangeStart + + expect($browser.url()).toEqual('http://server/'); + expect($location.url()).toEqual(''); + + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + throw Error('there is no before when user enters URL directly to browser'); + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl); + }); + + + $browser.url('http://server/#/somePath'); + $browser.poll(); + + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/somePath', 'http://server/']); + }) + ); + + + it('should listen on click events on href and prevent browser default in hasbang mode', function() { + module(function() { + return function($rootElement, $compile, $rootScope) { + $rootElement.html('link'); + $compile($rootElement)($rootScope); + jqLite(document.body).append($rootElement); + } + }); + + inject(function($location, $rootScope, $browser, $rootElement) { + var log = '', + link = $rootElement.find('a'); + + + $rootScope.$on('$locationChangeStart', function(event) { + event.preventDefault(); + log += '$locationChangeStart'; + }); + $rootScope.$on('$locationChangeCompleted', function() { + throw new Error('after cancellation in hashbang mode'); + }); + + browserTrigger(link, 'click'); + + expect(log).toEqual('$locationChangeStart'); + expect($browser.url()).toEqual('http://server/'); + + dealoc($rootElement); + }); + }); + + + it('should listen on click events on href and prevent browser default in html5 mode', function() { + module(function($locationProvider) { + $locationProvider.html5Mode(true); + return function($rootElement, $compile, $rootScope) { + $rootElement.html('link'); + $compile($rootElement)($rootScope); + jqLite(document.body).append($rootElement); + } + }); + + inject(function($location, $rootScope, $browser, $rootElement) { + var log = '', + link = $rootElement.find('a'); + + $rootScope.$on('$locationChangeStart', function(event) { + event.preventDefault(); + log += '$locationChangeStart'; + }); + $rootScope.$on('$locationChangeCompleted', function() { + throw new Error('after cancalation in html5 mode'); + }); + + browserTrigger(link, 'click'); + + expect(log).toEqual('$locationChangeStart'); + expect($browser.url()).toEqual('http://server/'); + + dealoc($rootElement); + }); + }); + }); }); diff --git a/test/ng/routeSpec.js b/test/ng/routeSpec.js index 8c5f93f9226a..31d932f72366 100644 --- a/test/ng/routeSpec.js +++ b/test/ng/routeSpec.js @@ -60,6 +60,28 @@ describe('$route', function() { }); + it('should not change route when location is canceled', function() { + module(function($routeProvider) { + $routeProvider.when('/somePath', {template: 'some path'}); + }); + inject(function($route, $location, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event) { + $log.info('$locationChangeStart'); + event.preventDefault(); + }); + + $rootScope.$on('$beforeRouteChange', function(event) { + throw new Error('Should not get here'); + }); + + $location.path('/somePath'); + $rootScope.$digest(); + + expect($log.info.logs.shift()).toEqual(['$locationChangeStart']); + }); + }); + + it('should match a route that contains special chars in the path', function() { module(function($routeProvider) { $routeProvider.when('/$test.23/foo(bar)/:baz', {templateUrl: 'test.html'}); @@ -540,8 +562,11 @@ describe('$route', function() { }); inject(function($route, $location, $rootScope) { var replace; - $rootScope.$watch(function() { - if (isUndefined(replace)) replace = $location.$$replace; + + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + if (oldUrl == 'http://server/#/foo/id3/eId') { + replace = $location.$$replace; + } }); $location.path('/foo/id3/eId');