Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 2614d4f

Browse files
committed
Merge branch 'master' of https://github.com/angular/angular.js into add-offset-to-$anchorScroll
2 parents bd56350 + 74a214c commit 2614d4f

File tree

13 files changed

+743
-125
lines changed

13 files changed

+743
-125
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@ngdoc error
2+
@name $location:nostate
3+
@fullName History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API
4+
@description
5+
6+
This error occurs when the {@link ng.$location#state $location.state} method is used when {@link ng.$locationProvider#html5Mode $locationProvider.html5Mode} is not turned on or the browser used doesn't support the HTML5 History API (for example, IE9 or Android 2.3).
7+
8+
To avoid this error, either drop support for those older browsers or avoid using this method.

src/Angular.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,10 @@ var /** holds major version number for IE or NaN for real browsers */
173173
uid = 0;
174174

175175
/**
176-
* IE 11 changed the format of the UserAgent string.
177-
* See http://msdn.microsoft.com/en-us/library/ms537503.aspx
176+
* documentMode is an IE-only property
177+
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
178178
*/
179-
msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]);
180-
if (isNaN(msie)) {
181-
msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]);
182-
}
179+
msie = document.documentMode;
183180

184181

185182
/**

src/auto/injector.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function annotate(fn, strictDi, name) {
134134
* expect($injector.get('$injector')).toBe($injector);
135135
* expect($injector.invoke(function($injector) {
136136
* return $injector;
137-
* }).toBe($injector);
137+
* })).toBe($injector);
138138
* ```
139139
*
140140
* # Injection Function Annotation

src/ng/browser.js

+46-21
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,9 @@ function Browser(window, document, $log, $sniffer) {
124124
//////////////////////////////////////////////////////////////
125125

126126
var lastBrowserUrl = location.href,
127+
lastHistoryState = history.state,
127128
baseElement = document.find('base'),
128-
newLocation = null;
129+
reloadLocation = null;
129130

130131
/**
131132
* @name $browser#url
@@ -144,29 +145,42 @@ function Browser(window, document, $log, $sniffer) {
144145
* {@link ng.$location $location service} to change url.
145146
*
146147
* @param {string} url New url (when used as setter)
147-
* @param {boolean=} replace Should new url replace current history record ?
148+
* @param {boolean=} replace Should new url replace current history record?
149+
* @param {object=} state object to use with pushState/replaceState
148150
*/
149-
self.url = function(url, replace) {
151+
self.url = function(url, replace, state) {
152+
// In modern browsers `history.state` is `null` by default; treating it separately
153+
// from `undefined` would cause `$browser.url('/foo')` to change `history.state`
154+
// to undefined via `pushState`. Instead, let's change `undefined` to `null` here.
155+
if (isUndefined(state)) {
156+
state = null;
157+
}
158+
150159
// Android Browser BFCache causes location, history reference to become stale.
151160
if (location !== window.location) location = window.location;
152161
if (history !== window.history) history = window.history;
153162

154163
// setter
155164
if (url) {
156-
if (lastBrowserUrl == url) return;
165+
// Don't change anything if previous and current URLs and states match. This also prevents
166+
// IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
167+
// See https://github.com/angular/angular.js/commit/ffb2701
168+
if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) {
169+
return;
170+
}
157171
var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
158172
lastBrowserUrl = url;
159173
// Don't use history API if only the hash changed
160174
// due to a bug in IE10/IE11 which leads
161175
// to not firing a `hashchange` nor `popstate` event
162176
// in some cases (see #9143).
163-
if (!sameBase && $sniffer.history) {
164-
if (replace) history.replaceState(null, '', url);
165-
else {
166-
history.pushState(null, '', url);
167-
}
177+
if ($sniffer.history && (!sameBase || history.state !== state)) {
178+
history[replace ? 'replaceState' : 'pushState'](state, '', url);
179+
lastHistoryState = history.state;
168180
} else {
169-
newLocation = url;
181+
if (!sameBase) {
182+
reloadLocation = url;
183+
}
170184
if (replace) {
171185
location.replace(url);
172186
} else {
@@ -176,27 +190,38 @@ function Browser(window, document, $log, $sniffer) {
176190
return self;
177191
// getter
178192
} else {
179-
// - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
180-
// methods not updating location.href synchronously.
193+
// - reloadLocation is needed as browsers don't allow to read out
194+
// the new location.href if a reload happened.
181195
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
182-
return newLocation || location.href.replace(/%27/g,"'");
196+
return reloadLocation || location.href.replace(/%27/g,"'");
183197
}
184198
};
185199

200+
/**
201+
* @name $browser#state
202+
*
203+
* @description
204+
* This method is a getter.
205+
*
206+
* Return history.state or null if history.state is undefined.
207+
*
208+
* @returns {object} state
209+
*/
210+
self.state = function() {
211+
return isUndefined(history.state) ? null : history.state;
212+
};
213+
186214
var urlChangeListeners = [],
187215
urlChangeInit = false;
188216

189217
function fireUrlChange() {
190-
newLocation = null;
191-
checkUrlChange();
192-
}
193-
194-
function checkUrlChange() {
195-
if (lastBrowserUrl == self.url()) return;
218+
if (lastBrowserUrl === self.url() && lastHistoryState === history.state) {
219+
return;
220+
}
196221

197222
lastBrowserUrl = self.url();
198223
forEach(urlChangeListeners, function(listener) {
199-
listener(self.url());
224+
listener(self.url(), history.state);
200225
});
201226
}
202227

@@ -247,7 +272,7 @@ function Browser(window, document, $log, $sniffer) {
247272
* Needs to be exported to be able to check for changes that have been done in sync,
248273
* as hashchange/popstate events fire in async.
249274
*/
250-
self.$$checkUrlChange = checkUrlChange;
275+
self.$$checkUrlChange = fireUrlChange;
251276

252277
//////////////////////////////////////////////////////////////
253278
// Misc API

src/ng/location.js

+110-30
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
303303
}
304304

305305

306-
LocationHashbangInHtml5Url.prototype =
307-
LocationHashbangUrl.prototype =
308-
LocationHtml5Url.prototype = {
306+
var locationPrototype = {
309307

310308
/**
311309
* Are we in html5 mode?
@@ -314,7 +312,7 @@ LocationHashbangInHtml5Url.prototype =
314312
$$html5: false,
315313

316314
/**
317-
* Has any change been replacing ?
315+
* Has any change been replacing?
318316
* @private
319317
*/
320318
$$replace: false,
@@ -530,6 +528,46 @@ LocationHashbangInHtml5Url.prototype =
530528
}
531529
};
532530

531+
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) {
532+
Location.prototype = Object.create(locationPrototype);
533+
534+
/**
535+
* @ngdoc method
536+
* @name $location#state
537+
*
538+
* @description
539+
* This method is getter / setter.
540+
*
541+
* Return the history state object when called without any parameter.
542+
*
543+
* Change the history state object when called with one parameter and return `$location`.
544+
* The state object is later passed to `pushState` or `replaceState`.
545+
*
546+
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting
547+
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
548+
* older browsers (like IE9 or Android < 4.0), don't use this method.
549+
*
550+
* @param {object=} state State object for pushState or replaceState
551+
* @return {object} state
552+
*/
553+
Location.prototype.state = function(state) {
554+
if (!arguments.length)
555+
return this.$$state;
556+
557+
if (Location !== LocationHtml5Url || !this.$$html5) {
558+
throw $locationMinErr('nostate', 'History API state support is available only ' +
559+
'in HTML5 mode and only in browsers supporting HTML5 History API');
560+
}
561+
// The user might modify `stateObject` after invoking `$location.state(stateObject)`
562+
// but we're changing the $$state reference to $browser.state() during the $digest
563+
// so the modification window is narrow.
564+
this.$$state = isUndefined(state) ? null : state;
565+
566+
return this;
567+
};
568+
});
569+
570+
533571
function locationGetter(property) {
534572
return function() {
535573
return this[property];
@@ -649,9 +687,14 @@ function $LocationProvider(){
649687
* details about event object. Upon successful change
650688
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
651689
*
690+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
691+
* the browser supports the HTML5 History API.
692+
*
652693
* @param {Object} angularEvent Synthetic event object.
653694
* @param {string} newUrl New URL
654695
* @param {string=} oldUrl URL that was before it was changed.
696+
* @param {string=} newState New history state object
697+
* @param {string=} oldState History state object that was before it was changed.
655698
*/
656699

657700
/**
@@ -661,9 +704,14 @@ function $LocationProvider(){
661704
* @description
662705
* Broadcasted after a URL was changed.
663706
*
707+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
708+
* the browser supports the HTML5 History API.
709+
*
664710
* @param {Object} angularEvent Synthetic event object.
665711
* @param {string} newUrl New URL
666712
* @param {string=} oldUrl URL that was before it was changed.
713+
* @param {string=} newState New history state object
714+
* @param {string=} oldState History state object that was before it was changed.
667715
*/
668716

669717
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
@@ -688,8 +736,29 @@ function $LocationProvider(){
688736
$location = new LocationMode(appBase, '#' + hashPrefix);
689737
$location.$$parseLinkUrl(initialUrl, initialUrl);
690738

739+
$location.$$state = $browser.state();
740+
691741
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
692742

743+
function setBrowserUrlWithFallback(url, replace, state) {
744+
var oldUrl = $location.url();
745+
var oldState = $location.$$state;
746+
try {
747+
$browser.url(url, replace, state);
748+
749+
// Make sure $location.state() returns referentially identical (not just deeply equal)
750+
// state object; this makes possible quick checking if the state changed in the digest
751+
// loop. Checking deep equality would be too expensive.
752+
$location.$$state = $browser.state();
753+
} catch (e) {
754+
// Restore old values if pushState fails
755+
$location.url(oldUrl);
756+
$location.$$state = oldState;
757+
758+
throw e;
759+
}
760+
}
761+
693762
$rootElement.on('click', function(event) {
694763
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
695764
// currently we open nice url link and redirect then
@@ -740,52 +809,63 @@ function $LocationProvider(){
740809
$browser.url($location.absUrl(), true);
741810
}
742811

743-
// update $location when $browser url changes
744-
$browser.onUrlChange(function(newUrl) {
745-
if ($location.absUrl() != newUrl) {
746-
$rootScope.$evalAsync(function() {
747-
var oldUrl = $location.absUrl();
812+
var initializing = true;
748813

749-
$location.$$parse(newUrl);
750-
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
751-
oldUrl).defaultPrevented) {
752-
$location.$$parse(oldUrl);
753-
$browser.url(oldUrl);
754-
} else {
755-
afterLocationChange(oldUrl);
756-
}
757-
});
758-
if (!$rootScope.$$phase) $rootScope.$digest();
759-
}
814+
// update $location when $browser url changes
815+
$browser.onUrlChange(function(newUrl, newState) {
816+
$rootScope.$evalAsync(function() {
817+
var oldUrl = $location.absUrl();
818+
var oldState = $location.$$state;
819+
820+
$location.$$parse(newUrl);
821+
$location.$$state = newState;
822+
if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
823+
newState, oldState).defaultPrevented) {
824+
$location.$$parse(oldUrl);
825+
$location.$$state = oldState;
826+
setBrowserUrlWithFallback(oldUrl, false, oldState);
827+
} else {
828+
initializing = false;
829+
afterLocationChange(oldUrl, oldState);
830+
}
831+
});
832+
if (!$rootScope.$$phase) $rootScope.$digest();
760833
});
761834

762835
// update browser
763-
var changeCounter = 0;
764836
$rootScope.$watch(function $locationWatch() {
765837
var oldUrl = $browser.url();
838+
var oldState = $browser.state();
766839
var currentReplace = $location.$$replace;
767840

768-
if (!changeCounter || oldUrl != $location.absUrl()) {
769-
changeCounter++;
841+
if (initializing || oldUrl !== $location.absUrl() ||
842+
($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) {
843+
initializing = false;
844+
770845
$rootScope.$evalAsync(function() {
771-
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
772-
defaultPrevented) {
846+
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl,
847+
$location.$$state, oldState).defaultPrevented) {
773848
$location.$$parse(oldUrl);
849+
$location.$$state = oldState;
774850
} else {
775-
$browser.url($location.absUrl(), currentReplace);
776-
afterLocationChange(oldUrl);
851+
setBrowserUrlWithFallback($location.absUrl(), currentReplace,
852+
oldState === $location.$$state ? null : $location.$$state);
853+
afterLocationChange(oldUrl, oldState);
777854
}
778855
});
779856
}
857+
780858
$location.$$replace = false;
781859

782-
return changeCounter;
860+
// we don't need to return anything because $evalAsync will make the digest loop dirty when
861+
// there is a change
783862
});
784863

785864
return $location;
786865

787-
function afterLocationChange(oldUrl) {
788-
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
866+
function afterLocationChange(oldUrl, oldState) {
867+
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
868+
$location.$$state, oldState);
789869
}
790870
}];
791871
}

0 commit comments

Comments
 (0)