mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
390 lines
13 KiB
JavaScript
390 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'),
|
|
newLocation = 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 {
|
|
newLocation = url;
|
|
if (replace) {
|
|
location.replace(url);
|
|
} else {
|
|
location.href = url;
|
|
}
|
|
}
|
|
return self;
|
|
// getter
|
|
} else {
|
|
// - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
|
|
// methods not updating location.href synchronously.
|
|
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
|
|
return newLocation || location.href.replace(/%27/g,"'");
|
|
}
|
|
};
|
|
|
|
var urlChangeListeners = [],
|
|
urlChangeInit = false;
|
|
|
|
function fireUrlChange() {
|
|
newLocation = null;
|
|
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:
|
|
*
|
|
* - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify
|
|
* it
|
|
* - cookies(name, value) -> set name to value, if value is undefined delete the cookie
|
|
* - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that
|
|
* way)
|
|
*
|
|
* @returns {Object} Hash of all cookies (if called without any parameter)
|
|
*/
|
|
self.cookies = function(name, value) {
|
|
/* global escape: false, unescape: false */
|
|
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
|
|
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);
|
|
}];
|
|
}
|