mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-20 16:30:26 +00:00
In the Android browser, the BFCache maintains the state of JavaScript applications even when navigating to another app, so that going forward and back, to and from an application is very fast. Unfortunately, this can have undesired side effects. In this instance, the location variable was holding a reference to a stale window.location, and was throwing errors when going back to an Angular app after browsing to another site. This fix makes sure that location.url() includes a check to make sure that location is referencing the current window.location. Closes #4044
384 lines
13 KiB
JavaScript
384 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* ! This is a private undocumented service !
|
|
*
|
|
* @name ng.$browser
|
|
* @requires $log
|
|
* @description
|
|
* This object has two goals:
|
|
*
|
|
* - hide all the global state in the browser caused by the window object
|
|
* - abstract away all the browser specific features and inconsistencies
|
|
*
|
|
* For tests we provide {@link ngMock.$browser mock implementation} of the `$browser`
|
|
* service, which can be used for convenient testing of the application without the interaction with
|
|
* the real browser apis.
|
|
*/
|
|
/**
|
|
* @param {object} window The global window object.
|
|
* @param {object} document jQuery wrapped document.
|
|
* @param {function()} XHR XMLHttpRequest constructor.
|
|
* @param {object} $log console.log or an object with the same interface.
|
|
* @param {object} $sniffer $sniffer service
|
|
*/
|
|
function Browser(window, document, $log, $sniffer) {
|
|
var self = this,
|
|
rawDocument = document[0],
|
|
location = window.location,
|
|
history = window.history,
|
|
setTimeout = window.setTimeout,
|
|
clearTimeout = window.clearTimeout,
|
|
pendingDeferIds = {};
|
|
|
|
self.isMock = false;
|
|
|
|
var outstandingRequestCount = 0;
|
|
var outstandingRequestCallbacks = [];
|
|
|
|
// TODO(vojta): remove this temporary api
|
|
self.$$completeOutstandingRequest = completeOutstandingRequest;
|
|
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
|
|
|
|
/**
|
|
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
|
|
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
|
|
*/
|
|
function completeOutstandingRequest(fn) {
|
|
try {
|
|
fn.apply(null, sliceArgs(arguments, 1));
|
|
} finally {
|
|
outstandingRequestCount--;
|
|
if (outstandingRequestCount === 0) {
|
|
while(outstandingRequestCallbacks.length) {
|
|
try {
|
|
outstandingRequestCallbacks.pop()();
|
|
} catch (e) {
|
|
$log.error(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* Note: this method is used only by scenario runner
|
|
* TODO(vojta): prefix this method with $$ ?
|
|
* @param {function()} callback Function that will be called when no outstanding request
|
|
*/
|
|
self.notifyWhenNoOutstandingRequests = function(callback) {
|
|
// force browser to execute all pollFns - this is needed so that cookies and other pollers fire
|
|
// at some deterministic time in respect to the test runner's actions. Leaving things up to the
|
|
// regular poller would result in flaky tests.
|
|
forEach(pollFns, function(pollFn){ pollFn(); });
|
|
|
|
if (outstandingRequestCount === 0) {
|
|
callback();
|
|
} else {
|
|
outstandingRequestCallbacks.push(callback);
|
|
}
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// Poll Watcher API
|
|
//////////////////////////////////////////////////////////////
|
|
var pollFns = [],
|
|
pollTimeout;
|
|
|
|
/**
|
|
* @name ng.$browser#addPollFn
|
|
* @methodOf ng.$browser
|
|
*
|
|
* @param {function()} fn Poll function to add
|
|
*
|
|
* @description
|
|
* Adds a function to the list of functions that poller periodically executes,
|
|
* and starts polling if not started yet.
|
|
*
|
|
* @returns {function()} the added function
|
|
*/
|
|
self.addPollFn = function(fn) {
|
|
if (isUndefined(pollTimeout)) startPoller(100, setTimeout);
|
|
pollFns.push(fn);
|
|
return fn;
|
|
};
|
|
|
|
/**
|
|
* @param {number} interval How often should browser call poll functions (ms)
|
|
* @param {function()} setTimeout Reference to a real or fake `setTimeout` function.
|
|
*
|
|
* @description
|
|
* Configures the poller to run in the specified intervals, using the specified
|
|
* setTimeout fn and kicks it off.
|
|
*/
|
|
function startPoller(interval, setTimeout) {
|
|
(function check() {
|
|
forEach(pollFns, function(pollFn){ pollFn(); });
|
|
pollTimeout = setTimeout(check, interval);
|
|
})();
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// URL API
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
var lastBrowserUrl = location.href,
|
|
baseElement = document.find('base'),
|
|
replacedUrl = null;
|
|
|
|
/**
|
|
* @name ng.$browser#url
|
|
* @methodOf ng.$browser
|
|
*
|
|
* @description
|
|
* GETTER:
|
|
* Without any argument, this method just returns current value of location.href.
|
|
*
|
|
* SETTER:
|
|
* With at least one argument, this method sets url to new value.
|
|
* If html5 history api supported, pushState/replaceState is used, otherwise
|
|
* location.href/location.replace is used.
|
|
* Returns its own instance to allow chaining
|
|
*
|
|
* NOTE: this api is intended for use only by the $location service. Please use the
|
|
* {@link ng.$location $location service} to change url.
|
|
*
|
|
* @param {string} url New url (when used as setter)
|
|
* @param {boolean=} replace Should new url replace current history record ?
|
|
*/
|
|
self.url = function(url, replace) {
|
|
// Android Browser BFCache causes location reference to become stale.
|
|
if (location !== window.location) location = window.location;
|
|
|
|
// setter
|
|
if (url) {
|
|
if (lastBrowserUrl == url) return;
|
|
lastBrowserUrl = url;
|
|
if ($sniffer.history) {
|
|
if (replace) history.replaceState(null, '', url);
|
|
else {
|
|
history.pushState(null, '', url);
|
|
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
|
|
baseElement.attr('href', baseElement.attr('href'));
|
|
}
|
|
} else {
|
|
if (replace) {
|
|
location.replace(url);
|
|
replacedUrl = url;
|
|
} else {
|
|
location.href = url;
|
|
replacedUrl = null;
|
|
}
|
|
}
|
|
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,"'");
|
|
}
|
|
};
|
|
|
|
var urlChangeListeners = [],
|
|
urlChangeInit = false;
|
|
|
|
function fireUrlChange() {
|
|
if (lastBrowserUrl == self.url()) return;
|
|
|
|
lastBrowserUrl = self.url();
|
|
forEach(urlChangeListeners, function(listener) {
|
|
listener(self.url());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @name ng.$browser#onUrlChange
|
|
* @methodOf ng.$browser
|
|
* @TODO(vojta): refactor to use node's syntax for events
|
|
*
|
|
* @description
|
|
* Register callback function that will be called, when url changes.
|
|
*
|
|
* It's only called when the url is changed by outside of angular:
|
|
* - user types different url into address bar
|
|
* - user clicks on history (forward/back) button
|
|
* - user clicks on a link
|
|
*
|
|
* It's not called when url is changed by $browser.url() method
|
|
*
|
|
* The listener gets called with new url as parameter.
|
|
*
|
|
* NOTE: this api is intended for use only by the $location service. Please use the
|
|
* {@link ng.$location $location service} to monitor url changes in angular apps.
|
|
*
|
|
* @param {function(string)} listener Listener function to be called when url changes.
|
|
* @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous.
|
|
*/
|
|
self.onUrlChange = function(callback) {
|
|
if (!urlChangeInit) {
|
|
// We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera)
|
|
// don't fire popstate when user change the address bar and don't fire hashchange when url
|
|
// changed by push/replaceState
|
|
|
|
// html5 history api - popstate event
|
|
if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange);
|
|
// hashchange event
|
|
if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange);
|
|
// polling
|
|
else self.addPollFn(fireUrlChange);
|
|
|
|
urlChangeInit = true;
|
|
}
|
|
|
|
urlChangeListeners.push(callback);
|
|
return callback;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// Misc API
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* @name ng.$browser#baseHref
|
|
* @methodOf ng.$browser
|
|
*
|
|
* @description
|
|
* Returns current <base href>
|
|
* (always relative - without domain)
|
|
*
|
|
* @returns {string=} current <base href>
|
|
*/
|
|
self.baseHref = function() {
|
|
var href = baseElement.attr('href');
|
|
return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : '';
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// Cookies API
|
|
//////////////////////////////////////////////////////////////
|
|
var lastCookies = {};
|
|
var lastCookieString = '';
|
|
var cookiePath = self.baseHref();
|
|
|
|
/**
|
|
* @name ng.$browser#cookies
|
|
* @methodOf ng.$browser
|
|
*
|
|
* @param {string=} name Cookie name
|
|
* @param {string=} value Cookie value
|
|
*
|
|
* @description
|
|
* The cookies method provides a 'private' low level access to browser cookies.
|
|
* It is not meant to be used directly, use the $cookie service instead.
|
|
*
|
|
* The return values vary depending on the arguments that the method was called with as follows:
|
|
* <ul>
|
|
* <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li>
|
|
* <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li>
|
|
* <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li>
|
|
* </ul>
|
|
*
|
|
* @returns {Object} Hash of all cookies (if called without any parameter)
|
|
*/
|
|
self.cookies = function(name, value) {
|
|
var cookieLength, cookieArray, cookie, i, index;
|
|
|
|
if (name) {
|
|
if (value === undefined) {
|
|
rawDocument.cookie = escape(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
|
} else {
|
|
if (isString(value)) {
|
|
cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + ';path=' + cookiePath).length + 1;
|
|
|
|
// per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
|
|
// - 300 cookies
|
|
// - 20 cookies per unique domain
|
|
// - 4096 bytes per cookie
|
|
if (cookieLength > 4096) {
|
|
$log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+
|
|
cookieLength + " > 4096 bytes)!");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (rawDocument.cookie !== lastCookieString) {
|
|
lastCookieString = rawDocument.cookie;
|
|
cookieArray = lastCookieString.split("; ");
|
|
lastCookies = {};
|
|
|
|
for (i = 0; i < cookieArray.length; i++) {
|
|
cookie = cookieArray[i];
|
|
index = cookie.indexOf('=');
|
|
if (index > 0) { //ignore nameless cookies
|
|
var name = unescape(cookie.substring(0, index));
|
|
// the first value that is seen for a cookie is the most
|
|
// specific one. values for the same cookie name that
|
|
// follow are for less specific paths.
|
|
if (lastCookies[name] === undefined) {
|
|
lastCookies[name] = unescape(cookie.substring(index + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return lastCookies;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @name ng.$browser#defer
|
|
* @methodOf ng.$browser
|
|
* @param {function()} fn A function, who's execution should be deferred.
|
|
* @param {number=} [delay=0] of milliseconds to defer the function execution.
|
|
* @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
|
|
*
|
|
* @description
|
|
* 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
|
|
* via `$browser.defer.flush()`.
|
|
*
|
|
*/
|
|
self.defer = function(fn, delay) {
|
|
var timeoutId;
|
|
outstandingRequestCount++;
|
|
timeoutId = setTimeout(function() {
|
|
delete pendingDeferIds[timeoutId];
|
|
completeOutstandingRequest(fn);
|
|
}, delay || 0);
|
|
pendingDeferIds[timeoutId] = true;
|
|
return timeoutId;
|
|
};
|
|
|
|
|
|
/**
|
|
* @name ng.$browser#defer.cancel
|
|
* @methodOf ng.$browser.defer
|
|
*
|
|
* @description
|
|
* Cancels a deferred 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 successfully canceled.
|
|
*/
|
|
self.defer.cancel = function(deferId) {
|
|
if (pendingDeferIds[deferId]) {
|
|
delete pendingDeferIds[deferId];
|
|
clearTimeout(deferId);
|
|
completeOutstandingRequest(noop);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
}
|
|
|
|
function $BrowserProvider(){
|
|
this.$get = ['$window', '$log', '$sniffer', '$document',
|
|
function( $window, $log, $sniffer, $document){
|
|
return new Browser($window, $document, $log, $sniffer);
|
|
}];
|
|
}
|