Path: blob/trunk/third_party/closure/goog/history/history.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Browser history stack management class.16*17* The goog.History object allows a page to create history state without leaving18* the current document. This allows users to, for example, hit the browser's19* back button without leaving the current page.20*21* The history object can be instantiated in one of two modes. In user visible22* mode, the current history state is shown in the browser address bar as a23* document location fragment (the portion of the URL after the '#'). These24* addresses can be bookmarked, copied and pasted into another browser, and25* modified directly by the user like any other URL.26*27* If the history object is created in invisible mode, the user can still28* affect the state using the browser forward and back buttons, but the current29* state is not displayed in the browser address bar. These states are not30* bookmarkable or editable.31*32* It is possible to use both types of history object on the same page, but not33* currently recommended due to browser deficiencies.34*35* Tested to work in:36* <ul>37* <li>Firefox 1.0-4.038* <li>Internet Explorer 5.5-9.039* <li>Opera 9+40* <li>Safari 4+41* </ul>42*43* @author [email protected] (Shawn Brenneman)44* @see ../demos/history1.html45* @see ../demos/history2.html46*/4748/* Some browser specific implementation notes:49*50* Firefox (through version 2.0.0.1):51*52* Ideally, navigating inside the hidden iframe could be done using53* about:blank#state instead of a real page on the server. Setting the hash on54* about:blank creates history entries, but the hash is not recorded and is lost55* when the user hits the back button. This is true in Opera as well. A blank56* HTML page must be provided for invisible states to be recorded in the iframe57* hash.58*59* After leaving the page with the History object and returning to it (by60* hitting the back button from another site), the last state of the iframe is61* overwritten. The most recent state is saved in a hidden input field so the62* previous state can be restored.63*64* Firefox does not store the previous value of dynamically generated input65* elements. To save the state, the hidden element must be in the HTML document,66* either in the original source or added with document.write. If a reference67* to the input element is not provided as a constructor argument, then the68* history object creates one using document.write, in which case the history69* object must be created from a script in the body element of the page.70*71* Manually editing the address field to a different hash link prevents further72* updates to the address bar. The page continues to work as normal, but the73* address shown will be incorrect until the page is reloaded.74*75* NOTE(user): It should be noted that Firefox will URL encode any non-regular76* ascii character, along with |space|, ", <, and >, when added to the fragment.77* If you expect these characters in your tokens you should consider that78* setToken('<b>') would result in the history fragment "%3Cb%3E", and79* "espére" would show "esp%E8re". (IE allows unicode characters in the80* fragment)81*82* TODO(user): Should we encapsulate this escaping into the API for visible83* history and encode all characters that aren't supported by Firefox? It also84* needs to be optional so apps can elect to handle the escaping themselves.85*86*87* Internet Explorer (through version 7.0):88*89* IE does not modify the history stack when the document fragment is changed.90* We create history entries instead by using document.open and document.write91* into a hidden iframe.92*93* IE destroys the history stack when navigating from /foo.html#someFragment to94* /foo.html. The workaround is to always append the # to the URL. This is95* somewhat unfortunate when loading the page without any # specified, because96* a second "click" sound will play on load as the fragment is automatically97* appended. If the hash is always present, this can be avoided.98*99* Manually editing the hash in the address bar in IE6 and then hitting the back100* button can replace the page with a blank page. This is a Bad User Experience,101* but probably not preventable.102*103* IE also has a bug when the page is loaded via a server redirect, setting104* a new hash value on the window location will force a page reload. This will105* happen the first time setToken is called with a new token. The only known106* workaround is to force a client reload early, for example by setting107* window.location.hash = window.location.hash, which will otherwise be a no-op.108*109* Internet Explorer 8.0, Webkit 532.1 and Gecko 1.9.2:110*111* IE8 has introduced the support to the HTML5 onhashchange event, which means112* we don't have to do any polling to detect fragment changes. Chrome and113* Firefox have added it on their newer builds, wekbit 532.1 and gecko 1.9.2.114* http://www.w3.org/TR/html5/history.html115* NOTE(goto): it is important to note that the document needs to have the116* <!DOCTYPE html> tag to enable the IE8 HTML5 mode. If the tag is not present,117* IE8 will enter IE7 compatibility mode (which can also be enabled manually).118*119* Opera (through version 9.02):120*121* Navigating through pages at a rate faster than some threshold causes Opera122* to cancel all outstanding timeouts and intervals, including the location123* polling loop. Since this condition cannot be detected, common input events124* are captured to cause the loop to restart.125*126* location.replace is adding a history entry inside setHash_, despite127* documentation that suggests it should not.128*129*130* Safari (through version 2.0.4):131*132* After hitting the back button, the location.hash property is no longer133* readable from JavaScript. This is fixed in later WebKit builds, but not in134* currently shipping Safari. For now, the only recourse is to disable history135* states in Safari. Pages are still navigable via the History object, but the136* back button cannot restore previous states.137*138* Safari sets history states on navigation to a hashlink, but doesn't allow139* polling of the hash, so following actual anchor links in the page will create140* useless history entries. Using location.replace does not seem to prevent141* this. Not a terribly good user experience, but fixed in later Webkits.142*143*144* WebKit (nightly version 420+):145*146* This almost works. Returning to a page with an invisible history object does147* not restore the old state, however, and there is no pageshow event that fires148* in this browser. Holding off on finding a solution for now.149*150*151* HTML5 capable browsers (Firefox 4, Chrome, Safari 5)152*153* No known issues. The goog.history.Html5History class provides a simpler154* implementation more suitable for recent browsers. These implementations155* should be merged so the history class automatically invokes the correct156* implementation.157*/158159160goog.provide('goog.History');161goog.provide('goog.History.Event');162goog.provide('goog.History.EventType');163164goog.require('goog.Timer');165goog.require('goog.asserts');166goog.require('goog.dom');167goog.require('goog.dom.InputType');168goog.require('goog.dom.safe');169/** @suppress {extraRequire} */170goog.require('goog.events.Event');171goog.require('goog.events.EventHandler');172goog.require('goog.events.EventTarget');173goog.require('goog.events.EventType');174goog.require('goog.history.Event');175goog.require('goog.history.EventType');176goog.require('goog.html.SafeHtml');177goog.require('goog.html.TrustedResourceUrl');178goog.require('goog.labs.userAgent.device');179goog.require('goog.memoize');180goog.require('goog.string');181goog.require('goog.string.Const');182goog.require('goog.userAgent');183184185186/**187* A history management object. Can be instantiated in user-visible mode (uses188* the address fragment to manage state) or in hidden mode. This object should189* be created from a script in the document body before the document has190* finished loading.191*192* To store the hidden states in browsers other than IE, a hidden iframe is193* used. It must point to a valid html page on the same domain (which can and194* probably should be blank.)195*196* Sample instantiation and usage:197*198* <pre>199* // Instantiate history to use the address bar for state.200* var h = new goog.History();201* goog.events.listen(h, goog.history.EventType.NAVIGATE, navCallback);202* h.setEnabled(true);203*204* // Any changes to the location hash will call the following function.205* function navCallback(e) {206* alert('Navigated to state "' + e.token + '"');207* }208*209* // The history token can also be set from code directly.210* h.setToken('foo');211* </pre>212*213* @param {boolean=} opt_invisible True to use hidden history states instead of214* the user-visible location hash.215* @param {!goog.html.TrustedResourceUrl=} opt_blankPageUrl A URL to a216* blank page on the same server. Required if opt_invisible is true.217* This URL is also used as the src for the iframe used to track history218* state in IE (if not specified the iframe is not given a src attribute).219* Access is Denied error may occur in IE7 if the window's URL's scheme220* is https, and this URL is not specified.221* @param {HTMLInputElement=} opt_input The hidden input element to be used to222* store the history token. If not provided, a hidden input element will223* be created using document.write.224* @param {HTMLIFrameElement=} opt_iframe The hidden iframe that will be used by225* IE for pushing history state changes, or by all browsers if opt_invisible226* is true. If not provided, a hidden iframe element will be created using227* document.write.228* @constructor229* @extends {goog.events.EventTarget}230*/231goog.History = function(232opt_invisible, opt_blankPageUrl, opt_input, opt_iframe) {233goog.events.EventTarget.call(this);234235if (opt_invisible && !opt_blankPageUrl) {236throw Error('Can\'t use invisible history without providing a blank page.');237}238239var input;240if (opt_input) {241input = opt_input;242} else {243var inputId = 'history_state' + goog.History.historyCount_;244var inputHtml = goog.html.SafeHtml.create('input', {245type: goog.dom.InputType.TEXT,246name: inputId,247id: inputId,248style: goog.string.Const.from('display:none')249});250goog.dom.safe.documentWrite(document, inputHtml);251input = goog.dom.getElement(inputId);252}253254/**255* An input element that stores the current iframe state. Used to restore256* the state when returning to the page on non-IE browsers.257* @type {HTMLInputElement}258* @private259*/260this.hiddenInput_ = /** @type {HTMLInputElement} */ (input);261262/**263* The window whose location contains the history token fragment. This is264* the window that contains the hidden input. It's typically the top window.265* It is not necessarily the same window that the js code is loaded in.266* @type {Window}267* @private268*/269this.window_ = opt_input ?270goog.dom.getWindow(goog.dom.getOwnerDocument(opt_input)) :271window;272273/**274* The base URL for the hidden iframe. Must refer to a document in the275* same domain as the main page.276* @type {!goog.html.TrustedResourceUrl|undefined}277* @private278*/279this.iframeSrc_ = opt_blankPageUrl;280281if (goog.userAgent.IE && !opt_blankPageUrl) {282if (window.location.protocol == 'https') {283this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(284goog.string.Const.from('https:///'));285} else {286this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(287goog.string.Const.from('javascript:""'));288}289}290291/**292* A timer for polling the current history state for changes.293* @type {goog.Timer}294* @private295*/296this.timer_ = new goog.Timer(goog.History.PollingType.NORMAL);297this.registerDisposable(this.timer_);298299/**300* True if the state tokens are displayed in the address bar, false for hidden301* history states.302* @type {boolean}303* @private304*/305this.userVisible_ = !opt_invisible;306307/**308* An object to keep track of the history event listeners.309* @type {goog.events.EventHandler<!goog.History>}310* @private311*/312this.eventHandler_ = new goog.events.EventHandler(this);313314if (opt_invisible || goog.History.LEGACY_IE) {315var iframe;316if (opt_iframe) {317iframe = opt_iframe;318} else {319var iframeId = 'history_iframe' + goog.History.historyCount_;320// Using a "sandbox" attribute on the iframe might be possible, but321// this HTML didn't initially have it and when it was refactored322// to SafeHtml it was kept without it.323var iframeHtml = goog.html.SafeHtml.createIframe(this.iframeSrc_, null, {324id: iframeId,325style: goog.string.Const.from('display:none'),326sandbox: undefined327});328goog.dom.safe.documentWrite(document, iframeHtml);329iframe = goog.dom.getElement(iframeId);330}331332/**333* Internet Explorer uses a hidden iframe for all history changes. Other334* browsers use the iframe only for pushing invisible states.335* @type {HTMLIFrameElement}336* @private337*/338this.iframe_ = /** @type {HTMLIFrameElement} */ (iframe);339340/**341* Whether the hidden iframe has had a document written to it yet in this342* session.343* @type {boolean}344* @private345*/346this.unsetIframe_ = true;347}348349if (goog.History.LEGACY_IE) {350// IE relies on the hidden input to restore the history state from previous351// sessions, but input values are only restored after window.onload. Set up352// a callback to poll the value after the onload event.353this.eventHandler_.listen(354this.window_, goog.events.EventType.LOAD, this.onDocumentLoaded);355356/**357* IE-only variable for determining if the document has loaded.358* @type {boolean}359* @protected360*/361this.documentLoaded = false;362363/**364* IE-only variable for storing whether the history object should be enabled365* once the document finishes loading.366* @type {boolean}367* @private368*/369this.shouldEnable_ = false;370}371372// Set the initial history state.373if (this.userVisible_) {374this.setHash_(this.getToken(), true);375} else {376this.setIframeToken_(this.hiddenInput_.value);377}378379goog.History.historyCount_++;380};381goog.inherits(goog.History, goog.events.EventTarget);382383384/**385* Status of when the object is active and dispatching events.386* @type {boolean}387* @private388*/389goog.History.prototype.enabled_ = false;390391392/**393* Whether the object is performing polling with longer intervals. This can394* occur for instance when setting the location of the iframe when in invisible395* mode and the server that is hosting the blank html page is down. In FF, this396* will cause the location of the iframe to no longer be accessible, with397* permision denied exceptions being thrown on every access of the history398* token. When this occurs, the polling interval is elongated. This causes399* exceptions to be thrown at a lesser rate while allowing for the history400* object to resurrect itself when the html page becomes accessible.401* @type {boolean}402* @private403*/404goog.History.prototype.longerPolling_ = false;405406407/**408* The last token set by the history object, used to poll for changes.409* @type {?string}410* @private411*/412goog.History.prototype.lastToken_ = null;413414415/**416* Whether the browser supports HTML5 history management's onhashchange event.417* {@link http://www.w3.org/TR/html5/history.html}. IE 9 in compatibility mode418* indicates that onhashchange is in window, but testing reveals the event419* isn't actually fired.420* @return {boolean} Whether onhashchange is supported.421*/422goog.History.isOnHashChangeSupported = goog.memoize(function() {423return goog.userAgent.IE ? goog.userAgent.isDocumentModeOrHigher(8) :424'onhashchange' in goog.global;425});426427428/**429* Whether the current browser is Internet Explorer prior to version 8. Many IE430* specific workarounds developed before version 8 are unnecessary in more431* current versions.432* @type {boolean}433*/434goog.History.LEGACY_IE =435goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8);436437438/**439* Whether the browser always requires the hash to be present. Internet Explorer440* before version 8 will reload the HTML page if the hash is omitted.441* @type {boolean}442*/443goog.History.HASH_ALWAYS_REQUIRED = goog.History.LEGACY_IE;444445446/**447* If not null, polling in the user invisible mode will be disabled until this448* token is seen. This is used to prevent a race condition where the iframe449* hangs temporarily while the location is changed.450* @type {?string}451* @private452*/453goog.History.prototype.lockedToken_ = null;454455456/** @override */457goog.History.prototype.disposeInternal = function() {458goog.History.superClass_.disposeInternal.call(this);459this.eventHandler_.dispose();460this.setEnabled(false);461};462463464/**465* Starts or stops the History polling loop. When enabled, the History object466* will immediately fire an event for the current location. The caller can set467* up event listeners between the call to the constructor and the call to468* setEnabled.469*470* On IE, actual startup may be delayed until the iframe and hidden input471* element have been loaded and can be polled. This behavior is transparent to472* the caller.473*474* @param {boolean} enable Whether to enable the history polling loop.475*/476goog.History.prototype.setEnabled = function(enable) {477478if (enable == this.enabled_) {479return;480}481482if (goog.History.LEGACY_IE && !this.documentLoaded) {483// Wait until the document has actually loaded before enabling the484// object or any saved state from a previous session will be lost.485this.shouldEnable_ = enable;486return;487}488489if (enable) {490if (goog.userAgent.OPERA) {491// Capture events for common user input so we can restart the timer in492// Opera if it fails. Yes, this is distasteful. See operaDefibrillator_.493this.eventHandler_.listen(494this.window_.document, goog.History.INPUT_EVENTS_,495this.operaDefibrillator_);496} else if (goog.userAgent.GECKO) {497// Firefox will not restore the correct state after navigating away from498// and then back to the page with the history object. This can be fixed499// by restarting the history object on the pageshow event.500this.eventHandler_.listen(this.window_, 'pageshow', this.onShow_);501}502503// TODO(user): make HTML5 and invisible history work by listening to the504// iframe # changes instead of the window.505if (goog.History.isOnHashChangeSupported() && this.userVisible_) {506this.eventHandler_.listen(507this.window_, goog.events.EventType.HASHCHANGE, this.onHashChange_);508this.enabled_ = true;509this.dispatchEvent(new goog.history.Event(this.getToken(), false));510} else if (511!(goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) ||512this.documentLoaded) {513// Start dispatching history events if all necessary loading has514// completed (always true for browsers other than IE.)515this.eventHandler_.listen(516this.timer_, goog.Timer.TICK, goog.bind(this.check_, this, true));517518this.enabled_ = true;519520// Initialize last token at startup except on IE < 8, where the last token521// must only be set in conjunction with IFRAME updates, or the IFRAME will522// start out of sync and remove any pre-existing URI fragment.523if (!goog.History.LEGACY_IE) {524this.lastToken_ = this.getToken();525this.dispatchEvent(new goog.history.Event(this.getToken(), false));526}527528this.timer_.start();529}530531} else {532this.enabled_ = false;533this.eventHandler_.removeAll();534this.timer_.stop();535}536};537538539/**540* Callback for the window onload event in IE. This is necessary to read the541* value of the hidden input after restoring a history session. The value of542* input elements is not viewable until after window onload for some reason (the543* iframe state is similarly unavailable during the loading phase.) If544* setEnabled is called before the iframe has completed loading, the history545* object will actually be enabled at this point.546* @protected547*/548goog.History.prototype.onDocumentLoaded = function() {549this.documentLoaded = true;550551if (this.hiddenInput_.value) {552// Any saved value in the hidden input can only be read after the document553// has been loaded due to an IE limitation. Restore the previous state if554// it has been set.555this.setIframeToken_(this.hiddenInput_.value, true);556}557558this.setEnabled(this.shouldEnable_);559};560561562/**563* Handler for the Gecko pageshow event. Restarts the history object so that the564* correct state can be restored in the hash or iframe.565* @param {goog.events.BrowserEvent} e The browser event.566* @private567*/568goog.History.prototype.onShow_ = function(e) {569// NOTE(user): persisted is a property passed in the pageshow event that570// indicates whether the page is being persisted from the cache or is being571// loaded for the first time.572if (e.getBrowserEvent()['persisted']) {573this.setEnabled(false);574this.setEnabled(true);575}576};577578579/**580* Handles HTML5 onhashchange events on browsers where it is supported.581* This is very similar to {@link #check_}, except that it is not executed582* continuously. It is only used when583* {@code goog.History.isOnHashChangeSupported()} is true.584* @param {goog.events.BrowserEvent} e The browser event.585* @private586*/587goog.History.prototype.onHashChange_ = function(e) {588var hash = this.getLocationFragment_(this.window_);589if (hash != this.lastToken_) {590this.update_(hash, true);591}592};593594595/**596* @return {string} The current token.597*/598goog.History.prototype.getToken = function() {599if (this.lockedToken_ != null) {600return this.lockedToken_;601} else if (this.userVisible_) {602return this.getLocationFragment_(this.window_);603} else {604return this.getIframeToken_() || '';605}606};607608609/**610* Sets the history state. When user visible states are used, the URL fragment611* will be set to the provided token. Sometimes it is necessary to set the612* history token before the document title has changed, in this case IE's613* history drop down can be out of sync with the token. To get around this614* problem, the app can pass in a title to use with the hidden iframe.615* @param {string} token The history state identifier.616* @param {string=} opt_title Optional title used when setting the hidden iframe617* title in IE.618*/619goog.History.prototype.setToken = function(token, opt_title) {620this.setHistoryState_(token, false, opt_title);621};622623624/**625* Replaces the current history state without affecting the rest of the history626* stack.627* @param {string} token The history state identifier.628* @param {string=} opt_title Optional title used when setting the hidden iframe629* title in IE.630*/631goog.History.prototype.replaceToken = function(token, opt_title) {632this.setHistoryState_(token, true, opt_title);633};634635636/**637* Gets the location fragment for the current URL. We don't use location.hash638* directly as the browser helpfully urlDecodes the string for us which can639* corrupt the tokens. For example, if we want to store: label/%2Froot it would640* be returned as label//root.641* @param {Window} win The window object to use.642* @return {string} The fragment.643* @private644*/645goog.History.prototype.getLocationFragment_ = function(win) {646var href = win.location.href;647var index = href.indexOf('#');648return index < 0 ? '' : href.substring(index + 1);649};650651652/**653* Sets the history state. When user visible states are used, the URL fragment654* will be set to the provided token. Setting opt_replace to true will cause the655* navigation to occur, but will replace the current history entry without656* affecting the length of the stack.657*658* @param {string} token The history state identifier.659* @param {boolean} replace Set to replace the current history entry instead of660* appending a new history state.661* @param {string=} opt_title Optional title used when setting the hidden iframe662* title in IE.663* @private664*/665goog.History.prototype.setHistoryState_ = function(token, replace, opt_title) {666if (this.getToken() != token) {667if (this.userVisible_) {668this.setHash_(token, replace);669670if (!goog.History.isOnHashChangeSupported()) {671if (goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) {672// IE must save state using the iframe.673this.setIframeToken_(token, replace, opt_title);674}675}676677// This condition needs to be called even if678// goog.History.isOnHashChangeSupported() is true so the NAVIGATE event679// fires sychronously.680if (this.enabled_) {681this.check_(false);682}683} else {684// Fire the event immediately so that setting history is synchronous, but685// set a suspendToken so that polling doesn't trigger a 'back'.686this.setIframeToken_(token, replace);687this.lockedToken_ = this.lastToken_ = this.hiddenInput_.value = token;688this.dispatchEvent(new goog.history.Event(token, false));689}690}691};692693694/**695* Sets or replaces the URL fragment. The token does not need to be URL encoded696* according to the URL specification, though certain characters (like newline)697* are automatically stripped.698*699* If opt_replace is not set, non-IE browsers will append a new entry to the700* history list. Setting the hash does not affect the history stack in IE701* (unless there is a pre-existing named anchor for that hash.)702*703* Older versions of Webkit cannot query the location hash, but it still can be704* set. If we detect one of these versions, always replace instead of creating705* new history entries.706*707* window.location.replace replaces the current state from the history stack.708* http://www.whatwg.org/specs/web-apps/current-work/#dom-location-replace709* http://www.whatwg.org/specs/web-apps/current-work/#replacement-enabled710*711* @param {string} token The new string to set.712* @param {boolean=} opt_replace Set to true to replace the current token713* without appending a history entry.714* @private715*/716goog.History.prototype.setHash_ = function(token, opt_replace) {717// If the page uses a BASE element, setting location.hash directly will718// navigate away from the current document. Also, the original URL path may719// possibly change from HTML5 history pushState. To account for these, the720// full path is always specified.721var loc = this.window_.location;722var url = loc.href.split('#')[0];723724// If a hash has already been set, then removing it programmatically will725// reload the page. Once there is a hash, we won't remove it.726var hasHash = goog.string.contains(loc.href, '#');727728if (goog.History.HASH_ALWAYS_REQUIRED || hasHash || token) {729url += '#' + token;730}731732if (url != loc.href) {733if (opt_replace) {734loc.replace(url);735} else {736loc.href = url;737}738}739};740741742/**743* Sets the hidden iframe state. On IE, this is accomplished by writing a new744* document into the iframe. In Firefox, the iframe's URL fragment stores the745* state instead.746*747* Older versions of webkit cannot set the iframe, so ignore those browsers.748*749* @param {string} token The new string to set.750* @param {boolean=} opt_replace Set to true to replace the current iframe state751* without appending a new history entry.752* @param {string=} opt_title Optional title used when setting the hidden iframe753* title in IE.754* @private755*/756goog.History.prototype.setIframeToken_ = function(757token, opt_replace, opt_title) {758if (this.unsetIframe_ || token != this.getIframeToken_()) {759this.unsetIframe_ = false;760token = goog.string.urlEncode(token);761762if (goog.userAgent.IE) {763// Caching the iframe document results in document permission errors after764// leaving the page and returning. Access it anew each time instead.765var doc = goog.dom.getFrameContentDocument(this.iframe_);766767doc.open('text/html', opt_replace ? 'replace' : undefined);768var iframeSourceHtml = goog.html.SafeHtml.concat(769goog.html.SafeHtml.create(770'title', {}, (opt_title || this.window_.document.title)),771goog.html.SafeHtml.create('body', {}, token));772goog.dom.safe.documentWrite(doc, iframeSourceHtml);773doc.close();774} else {775goog.asserts.assertInstanceof(776this.iframeSrc_, goog.html.TrustedResourceUrl,777'this.iframeSrc_ must be set on calls to setIframeToken_');778var url =779goog.html.TrustedResourceUrl.unwrap(780/** @type {!goog.html.TrustedResourceUrl} */ (this.iframeSrc_)) +781'#' + token;782783// In Safari, it is possible for the contentWindow of the iframe to not784// be present when the page is loading after a reload.785var contentWindow = this.iframe_.contentWindow;786if (contentWindow) {787if (opt_replace) {788contentWindow.location.replace(url);789} else {790contentWindow.location.href = url;791}792}793}794}795};796797798/**799* Return the current state string from the hidden iframe. On internet explorer,800* this is stored as a string in the document body. Other browsers use the801* location hash of the hidden iframe.802*803* Older versions of webkit cannot access the iframe location, so always return804* null in that case.805*806* @return {?string} The state token saved in the iframe (possibly null if the807* iframe has never loaded.).808* @private809*/810goog.History.prototype.getIframeToken_ = function() {811if (goog.userAgent.IE) {812var doc = goog.dom.getFrameContentDocument(this.iframe_);813return doc.body ? goog.string.urlDecode(doc.body.innerHTML) : null;814} else {815// In Safari, it is possible for the contentWindow of the iframe to not816// be present when the page is loading after a reload.817var contentWindow = this.iframe_.contentWindow;818if (contentWindow) {819var hash;820821try {822// Iframe tokens are urlEncoded823hash = goog.string.urlDecode(this.getLocationFragment_(contentWindow));824} catch (e) {825// An exception will be thrown if the location of the iframe can not be826// accessed (permission denied). This can occur in FF if the the server827// that is hosting the blank html page goes down and then a new history828// token is set. The iframe will navigate to an error page, and the829// location of the iframe can no longer be accessed. Due to the polling,830// this will cause constant exceptions to be thrown. In this case,831// we enable longer polling. We do not have to attempt to reset the832// iframe token because (a) we already fired the NAVIGATE event when833// setting the token, (b) we can rely on the locked token for current834// state, and (c) the token is still in the history and835// accesible on forward/back.836if (!this.longerPolling_) {837this.setLongerPolling_(true);838}839840return null;841}842843// There was no exception when getting the hash so turn off longer polling844// if it is on.845if (this.longerPolling_) {846this.setLongerPolling_(false);847}848849return hash || null;850} else {851return null;852}853}854};855856857/**858* Checks the state of the document fragment and the iframe title to detect859* navigation changes. If {@code goog.HistoryisOnHashChangeSupported()} is860* {@code false}, then this runs approximately twenty times per second.861* @param {boolean} isNavigation True if the event was initiated by a browser862* action, false if it was caused by a setToken call. See863* {@link goog.history.Event}.864* @private865*/866goog.History.prototype.check_ = function(isNavigation) {867if (this.userVisible_) {868var hash = this.getLocationFragment_(this.window_);869if (hash != this.lastToken_) {870this.update_(hash, isNavigation);871}872}873874// Old IE uses the iframe for both visible and non-visible versions.875if (!this.userVisible_ || goog.History.LEGACY_IE) {876var token = this.getIframeToken_() || '';877if (this.lockedToken_ == null || token == this.lockedToken_) {878this.lockedToken_ = null;879if (token != this.lastToken_) {880this.update_(token, isNavigation);881}882}883}884};885886887/**888* Updates the current history state with a given token. Called after a change889* to the location or the iframe state is detected by poll_.890*891* @param {string} token The new history state.892* @param {boolean} isNavigation True if the event was initiated by a browser893* action, false if it was caused by a setToken call. See894* {@link goog.history.Event}.895* @private896*/897goog.History.prototype.update_ = function(token, isNavigation) {898this.lastToken_ = this.hiddenInput_.value = token;899900if (this.userVisible_) {901if (goog.History.LEGACY_IE) {902this.setIframeToken_(token);903}904905this.setHash_(token);906} else {907this.setIframeToken_(token);908}909910this.dispatchEvent(new goog.history.Event(this.getToken(), isNavigation));911};912913914/**915* Sets if the history oject should use longer intervals when polling.916*917* @param {boolean} longerPolling Whether to enable longer polling.918* @private919*/920goog.History.prototype.setLongerPolling_ = function(longerPolling) {921if (this.longerPolling_ != longerPolling) {922this.timer_.setInterval(923longerPolling ? goog.History.PollingType.LONG :924goog.History.PollingType.NORMAL);925}926this.longerPolling_ = longerPolling;927};928929930/**931* Opera cancels all outstanding timeouts and intervals after any rapid932* succession of navigation events, including the interval used to detect933* navigation events. This function restarts the interval so that navigation can934* continue. Ideally, only events which would be likely to cause a navigation935* change (mousedown and keydown) would be bound to this function. Since Opera936* seems to ignore keydown events while the alt key is pressed (such as937* alt-left or right arrow), this function is also bound to the much more938* frequent mousemove event. This way, when the update loop freezes, it will939* unstick itself as the user wiggles the mouse in frustration.940* @private941*/942goog.History.prototype.operaDefibrillator_ = function() {943this.timer_.stop();944this.timer_.start();945};946947948/**949* List of user input event types registered in Opera to restart the history950* timer (@see goog.History#operaDefibrillator_).951* @type {Array<string>}952* @private953*/954goog.History.INPUT_EVENTS_ = [955goog.events.EventType.MOUSEDOWN, goog.events.EventType.KEYDOWN,956goog.events.EventType.MOUSEMOVE957];958959960/**961* Counter for the number of goog.History objects that have been instantiated.962* Used to create unique IDs.963* @type {number}964* @private965*/966goog.History.historyCount_ = 0;967968969/**970* Types of polling. The values are in ms of the polling interval.971* @enum {number}972*/973goog.History.PollingType = {974NORMAL: 150,975LONG: 10000976};977978979/**980* Constant for the history change event type.981* @enum {string}982* @deprecated Use goog.history.EventType.983*/984goog.History.EventType = goog.history.EventType;985986987988/**989* Constant for the history change event type.990* @constructor991* @deprecated Use goog.history.Event.992* @final993*/994goog.History.Event = goog.history.Event;995996997