'use strict'; ////////////////////////////// // Browser ////////////////////////////// var XHR = window.XMLHttpRequest || function () { try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} throw new Error("This browser does not support XMLHttpRequest."); }; /** * @ngdoc service * @name angular.service.$browser * @requires $log * * @description * Constructor for the object exposed as $browser service. * * 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 angular.mock.service.$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 {object} body jQuery wrapped document.body. * @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, body, XHR, $log, $sniffer) { var self = this, rawDocument = document[0], location = window.location, history = window.history, setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, pendingDeferIds = {}; self.isMock = false; ////////////////////////////////////////////////////////////// // XHR API ////////////////////////////////////////////////////////////// var idCounter = 0; var outstandingRequestCount = 0; var outstandingRequestCallbacks = []; /** * 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); } } } } } /** * @workInProgress * @ngdoc method * @name angular.service.$browser#xhr * @methodOf angular.service.$browser * * @param {string} method Requested method (get|post|put|delete|head|json) * @param {string} url Requested url * @param {?string} post Post data to send (null if nothing to post) * @param {function(number, string)} callback Function that will be called on response * @param {object=} header additional HTTP headers to send with XHR. * Standard headers are: * * * @description * Send ajax request */ self.xhr = function(method, url, post, callback, headers) { outstandingRequestCount ++; if (lowercase(method) == 'json') { var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, ''); window[callbackId] = function(data) { window[callbackId].data = data; }; var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), function() { if (window[callbackId].data) { completeOutstandingRequest(callback, 200, window[callbackId].data); } else { completeOutstandingRequest(callback); } delete window[callbackId]; body[0].removeChild(script); }); } else { var xhr = new XHR(); xhr.open(method, url, true); forEach(headers, function(value, key) { if (value) xhr.setRequestHeader(key, value); }); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { // normalize IE bug (http://bugs.jquery.com/ticket/1450) var status = xhr.status == 1223 ? 204 : xhr.status; completeOutstandingRequest(callback, status, xhr.responseText); } }; xhr.send(post || ''); } }; /** * @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; /** * @workInProgress * @ngdoc method * @name angular.service.$browser#addPollFn * @methodOf angular.service.$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; /** * @workInProgress * @ngdoc method * @name angular.service.$browser#url * @methodOf angular.service.$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 angular.service.$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) { // setter if (url) { lastBrowserUrl = url; if ($sniffer.history) { if (replace) history.replaceState(null, '', url); else history.pushState(null, '', url); } else { if (replace) location.replace(url); else location.href = url; } return self; // getter } else { return location.href; } }; var urlChangeListeners = [], urlChangeInit = false; function fireUrlChange() { if (lastBrowserUrl == self.url()) return; lastBrowserUrl = self.url(); forEach(urlChangeListeners, function(listener) { listener(self.url()); }); } /** * @workInProgress * @ngdoc method * @name angular.service.$browser#onUrlChange * @methodOf angular.service.$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 angular.service.$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).bind('popstate', fireUrlChange); // hashchange event if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); // polling else self.addPollFn(fireUrlChange); urlChangeInit = true; } urlChangeListeners.push(callback); return callback; }; ////////////////////////////////////////////////////////////// // Cookies API ////////////////////////////////////////////////////////////// var lastCookies = {}; var lastCookieString = ''; /** * @workInProgress * @ngdoc method * @name angular.service.$browser#cookies * @methodOf angular.service.$browser * * @param {string=} name Cookie name * @param {string=} value Cokkie 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: * * * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function (name, value) { var cookieLength, cookieArray, cookie, i, keyValue, index; if (name) { if (value === undefined) { rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { rawDocument.cookie = escape(name) + '=' + escape(value); cookieLength = name.length + value.length + 1; if (cookieLength > 4096) { $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ cookieLength + " > 4096 bytes)!"); } if (lastCookies.length > 20) { $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + "were already set (" + lastCookies.length + " > 20 )"); } } } } 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 lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); } } } return lastCookies; } }; /** * @workInProgress * @ngdoc method * @name angular.service.$browser#defer * @methodOf angular.service.$browser * @param {function()} fn A function, who's execution should be defered. * @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 asynchroniously 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; }; /** * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method * * @name angular.service.$browser#defer.cancel * @methodOf angular.service.$browser.defer * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. * * @description * Cancels a defered task identified with `deferId`. */ self.defer.cancel = function(deferId) { if (pendingDeferIds[deferId]) { delete pendingDeferIds[deferId]; clearTimeout(deferId); completeOutstandingRequest(noop); return true; } }; ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// /** * @workInProgress * @ngdoc method * @name angular.service.$browser#addCss * @methodOf angular.service.$browser * * @param {string} url Url to css file * @description * Adds a stylesheet tag to the head. */ self.addCss = function(url) { var link = jqLite(rawDocument.createElement('link')); link.attr('rel', 'stylesheet'); link.attr('type', 'text/css'); link.attr('href', url); body.append(link); }; /** * @workInProgress * @ngdoc method * @name angular.service.$browser#addJs * @methodOf angular.service.$browser * * @param {string} url Url to js file * * @description * Adds a script tag to the head. */ self.addJs = function(url, done) { // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document var script = rawDocument.createElement('script'); script.type = 'text/javascript'; script.src = url; if (msie) { script.onreadystatechange = function() { /loaded|complete/.test(script.readyState) && done && done(); }; } else { if (done) script.onload = script.onerror = done; } body[0].appendChild(script); return script; }; /** * Returns current * (always relative - without domain) * * @returns {string=} */ self.baseHref = function() { var href = document.find('base').attr('href'); return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; }; }