diff options
| author | Misko Hevery | 2012-03-23 14:03:24 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2012-03-28 11:16:35 -0700 | 
| commit | 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch) | |
| tree | e7529b741d70199f36d52090b430510bad07f233 /src/ng/browser.js | |
| parent | 944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff) | |
| download | angular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2 | |
chore(module): move files around in preparation for more modules
Diffstat (limited to 'src/ng/browser.js')
| -rw-r--r-- | src/ng/browser.js | 413 | 
1 files changed, 413 insertions, 0 deletions
| diff --git a/src/ng/browser.js b/src/ng/browser.js new file mode 100644 index 00000000..97e9cf3e --- /dev/null +++ b/src/ng/browser.js @@ -0,0 +1,413 @@ +'use strict'; + +/** + * @ngdoc object + * @name angular.module.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 angular.module.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 {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, $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; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addPollFn +   * @methodOf angular.module.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; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#url +   * @methodOf angular.module.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 angular.module.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) { +    // 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()); +    }); +  } + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#onUrlChange +   * @methodOf angular.module.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 angular.module.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).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 = ''; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#cookies +   * @methodOf angular.module.ng.$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: +   * <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, 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; +    } +  }; + + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#defer +   * @methodOf angular.module.ng.$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.module.ng.$browser#defer.cancel +   * @methodOf angular.module.ng.$browser.defer +   * +   * @description +   * Cancels a defered 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 successfuly canceled. +   */ +  self.defer.cancel = function(deferId) { +    if (pendingDeferIds[deferId]) { +      delete pendingDeferIds[deferId]; +      clearTimeout(deferId); +      completeOutstandingRequest(noop); +      return true; +    } +    return false; +  }; + + +  ////////////////////////////////////////////////////////////// +  // Misc API +  ////////////////////////////////////////////////////////////// + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addCss +   * @methodOf angular.module.ng.$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); +  }; + + +  /** +   * @ngdoc method +   * @name angular.module.ng.$browser#addJs +   * @methodOf angular.module.ng.$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 <base href> +   * (always relative - without domain) +   * +   * @returns {string=} +   */ +  self.baseHref = function() { +    var href = document.find('base').attr('href'); +    return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; +  }; +} + +function $BrowserProvider(){ +  this.$get = ['$window', '$log', '$sniffer', '$document', +      function( $window,   $log,   $sniffer,   $document){ +        return new Browser($window, $document, $document.find('body'), $log, $sniffer); +      }]; +} | 
