history.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Browser history stack management class.
  16. *
  17. * The goog.History object allows a page to create history state without leaving
  18. * the current document. This allows users to, for example, hit the browser's
  19. * back button without leaving the current page.
  20. *
  21. * The history object can be instantiated in one of two modes. In user visible
  22. * mode, the current history state is shown in the browser address bar as a
  23. * document location fragment (the portion of the URL after the '#'). These
  24. * addresses can be bookmarked, copied and pasted into another browser, and
  25. * modified directly by the user like any other URL.
  26. *
  27. * If the history object is created in invisible mode, the user can still
  28. * affect the state using the browser forward and back buttons, but the current
  29. * state is not displayed in the browser address bar. These states are not
  30. * bookmarkable or editable.
  31. *
  32. * It is possible to use both types of history object on the same page, but not
  33. * currently recommended due to browser deficiencies.
  34. *
  35. * Tested to work in:
  36. * <ul>
  37. * <li>Firefox 1.0-4.0
  38. * <li>Internet Explorer 5.5-9.0
  39. * <li>Opera 9+
  40. * <li>Safari 4+
  41. * </ul>
  42. *
  43. * @author brenneman@google.com (Shawn Brenneman)
  44. * @see ../demos/history1.html
  45. * @see ../demos/history2.html
  46. */
  47. /* Some browser specific implementation notes:
  48. *
  49. * Firefox (through version 2.0.0.1):
  50. *
  51. * Ideally, navigating inside the hidden iframe could be done using
  52. * about:blank#state instead of a real page on the server. Setting the hash on
  53. * about:blank creates history entries, but the hash is not recorded and is lost
  54. * when the user hits the back button. This is true in Opera as well. A blank
  55. * HTML page must be provided for invisible states to be recorded in the iframe
  56. * hash.
  57. *
  58. * After leaving the page with the History object and returning to it (by
  59. * hitting the back button from another site), the last state of the iframe is
  60. * overwritten. The most recent state is saved in a hidden input field so the
  61. * previous state can be restored.
  62. *
  63. * Firefox does not store the previous value of dynamically generated input
  64. * elements. To save the state, the hidden element must be in the HTML document,
  65. * either in the original source or added with document.write. If a reference
  66. * to the input element is not provided as a constructor argument, then the
  67. * history object creates one using document.write, in which case the history
  68. * object must be created from a script in the body element of the page.
  69. *
  70. * Manually editing the address field to a different hash link prevents further
  71. * updates to the address bar. The page continues to work as normal, but the
  72. * address shown will be incorrect until the page is reloaded.
  73. *
  74. * NOTE(user): It should be noted that Firefox will URL encode any non-regular
  75. * ascii character, along with |space|, ", <, and >, when added to the fragment.
  76. * If you expect these characters in your tokens you should consider that
  77. * setToken('<b>') would result in the history fragment "%3Cb%3E", and
  78. * "esp&eacute;re" would show "esp%E8re". (IE allows unicode characters in the
  79. * fragment)
  80. *
  81. * TODO(user): Should we encapsulate this escaping into the API for visible
  82. * history and encode all characters that aren't supported by Firefox? It also
  83. * needs to be optional so apps can elect to handle the escaping themselves.
  84. *
  85. *
  86. * Internet Explorer (through version 7.0):
  87. *
  88. * IE does not modify the history stack when the document fragment is changed.
  89. * We create history entries instead by using document.open and document.write
  90. * into a hidden iframe.
  91. *
  92. * IE destroys the history stack when navigating from /foo.html#someFragment to
  93. * /foo.html. The workaround is to always append the # to the URL. This is
  94. * somewhat unfortunate when loading the page without any # specified, because
  95. * a second "click" sound will play on load as the fragment is automatically
  96. * appended. If the hash is always present, this can be avoided.
  97. *
  98. * Manually editing the hash in the address bar in IE6 and then hitting the back
  99. * button can replace the page with a blank page. This is a Bad User Experience,
  100. * but probably not preventable.
  101. *
  102. * IE also has a bug when the page is loaded via a server redirect, setting
  103. * a new hash value on the window location will force a page reload. This will
  104. * happen the first time setToken is called with a new token. The only known
  105. * workaround is to force a client reload early, for example by setting
  106. * window.location.hash = window.location.hash, which will otherwise be a no-op.
  107. *
  108. * Internet Explorer 8.0, Webkit 532.1 and Gecko 1.9.2:
  109. *
  110. * IE8 has introduced the support to the HTML5 onhashchange event, which means
  111. * we don't have to do any polling to detect fragment changes. Chrome and
  112. * Firefox have added it on their newer builds, wekbit 532.1 and gecko 1.9.2.
  113. * http://www.w3.org/TR/html5/history.html
  114. * NOTE(goto): it is important to note that the document needs to have the
  115. * <!DOCTYPE html> tag to enable the IE8 HTML5 mode. If the tag is not present,
  116. * IE8 will enter IE7 compatibility mode (which can also be enabled manually).
  117. *
  118. * Opera (through version 9.02):
  119. *
  120. * Navigating through pages at a rate faster than some threshold causes Opera
  121. * to cancel all outstanding timeouts and intervals, including the location
  122. * polling loop. Since this condition cannot be detected, common input events
  123. * are captured to cause the loop to restart.
  124. *
  125. * location.replace is adding a history entry inside setHash_, despite
  126. * documentation that suggests it should not.
  127. *
  128. *
  129. * Safari (through version 2.0.4):
  130. *
  131. * After hitting the back button, the location.hash property is no longer
  132. * readable from JavaScript. This is fixed in later WebKit builds, but not in
  133. * currently shipping Safari. For now, the only recourse is to disable history
  134. * states in Safari. Pages are still navigable via the History object, but the
  135. * back button cannot restore previous states.
  136. *
  137. * Safari sets history states on navigation to a hashlink, but doesn't allow
  138. * polling of the hash, so following actual anchor links in the page will create
  139. * useless history entries. Using location.replace does not seem to prevent
  140. * this. Not a terribly good user experience, but fixed in later Webkits.
  141. *
  142. *
  143. * WebKit (nightly version 420+):
  144. *
  145. * This almost works. Returning to a page with an invisible history object does
  146. * not restore the old state, however, and there is no pageshow event that fires
  147. * in this browser. Holding off on finding a solution for now.
  148. *
  149. *
  150. * HTML5 capable browsers (Firefox 4, Chrome, Safari 5)
  151. *
  152. * No known issues. The goog.history.Html5History class provides a simpler
  153. * implementation more suitable for recent browsers. These implementations
  154. * should be merged so the history class automatically invokes the correct
  155. * implementation.
  156. */
  157. goog.provide('goog.History');
  158. goog.provide('goog.History.Event');
  159. goog.provide('goog.History.EventType');
  160. goog.require('goog.Timer');
  161. goog.require('goog.asserts');
  162. goog.require('goog.dom');
  163. goog.require('goog.dom.InputType');
  164. goog.require('goog.dom.safe');
  165. /** @suppress {extraRequire} */
  166. goog.require('goog.events.Event');
  167. goog.require('goog.events.EventHandler');
  168. goog.require('goog.events.EventTarget');
  169. goog.require('goog.events.EventType');
  170. goog.require('goog.history.Event');
  171. goog.require('goog.history.EventType');
  172. goog.require('goog.html.SafeHtml');
  173. goog.require('goog.html.TrustedResourceUrl');
  174. goog.require('goog.labs.userAgent.device');
  175. goog.require('goog.memoize');
  176. goog.require('goog.string');
  177. goog.require('goog.string.Const');
  178. goog.require('goog.userAgent');
  179. /**
  180. * A history management object. Can be instantiated in user-visible mode (uses
  181. * the address fragment to manage state) or in hidden mode. This object should
  182. * be created from a script in the document body before the document has
  183. * finished loading.
  184. *
  185. * To store the hidden states in browsers other than IE, a hidden iframe is
  186. * used. It must point to a valid html page on the same domain (which can and
  187. * probably should be blank.)
  188. *
  189. * Sample instantiation and usage:
  190. *
  191. * <pre>
  192. * // Instantiate history to use the address bar for state.
  193. * var h = new goog.History();
  194. * goog.events.listen(h, goog.history.EventType.NAVIGATE, navCallback);
  195. * h.setEnabled(true);
  196. *
  197. * // Any changes to the location hash will call the following function.
  198. * function navCallback(e) {
  199. * alert('Navigated to state "' + e.token + '"');
  200. * }
  201. *
  202. * // The history token can also be set from code directly.
  203. * h.setToken('foo');
  204. * </pre>
  205. *
  206. * @param {boolean=} opt_invisible True to use hidden history states instead of
  207. * the user-visible location hash.
  208. * @param {!goog.html.TrustedResourceUrl=} opt_blankPageUrl A URL to a
  209. * blank page on the same server. Required if opt_invisible is true.
  210. * This URL is also used as the src for the iframe used to track history
  211. * state in IE (if not specified the iframe is not given a src attribute).
  212. * Access is Denied error may occur in IE7 if the window's URL's scheme
  213. * is https, and this URL is not specified.
  214. * @param {HTMLInputElement=} opt_input The hidden input element to be used to
  215. * store the history token. If not provided, a hidden input element will
  216. * be created using document.write.
  217. * @param {HTMLIFrameElement=} opt_iframe The hidden iframe that will be used by
  218. * IE for pushing history state changes, or by all browsers if opt_invisible
  219. * is true. If not provided, a hidden iframe element will be created using
  220. * document.write.
  221. * @constructor
  222. * @extends {goog.events.EventTarget}
  223. */
  224. goog.History = function(
  225. opt_invisible, opt_blankPageUrl, opt_input, opt_iframe) {
  226. goog.events.EventTarget.call(this);
  227. if (opt_invisible && !opt_blankPageUrl) {
  228. throw Error('Can\'t use invisible history without providing a blank page.');
  229. }
  230. var input;
  231. if (opt_input) {
  232. input = opt_input;
  233. } else {
  234. var inputId = 'history_state' + goog.History.historyCount_;
  235. var inputHtml = goog.html.SafeHtml.create('input', {
  236. type: goog.dom.InputType.TEXT,
  237. name: inputId,
  238. id: inputId,
  239. style: goog.string.Const.from('display:none')
  240. });
  241. goog.dom.safe.documentWrite(document, inputHtml);
  242. input = goog.dom.getElement(inputId);
  243. }
  244. /**
  245. * An input element that stores the current iframe state. Used to restore
  246. * the state when returning to the page on non-IE browsers.
  247. * @type {HTMLInputElement}
  248. * @private
  249. */
  250. this.hiddenInput_ = /** @type {HTMLInputElement} */ (input);
  251. /**
  252. * The window whose location contains the history token fragment. This is
  253. * the window that contains the hidden input. It's typically the top window.
  254. * It is not necessarily the same window that the js code is loaded in.
  255. * @type {Window}
  256. * @private
  257. */
  258. this.window_ = opt_input ?
  259. goog.dom.getWindow(goog.dom.getOwnerDocument(opt_input)) :
  260. window;
  261. /**
  262. * The base URL for the hidden iframe. Must refer to a document in the
  263. * same domain as the main page.
  264. * @type {!goog.html.TrustedResourceUrl|undefined}
  265. * @private
  266. */
  267. this.iframeSrc_ = opt_blankPageUrl;
  268. if (goog.userAgent.IE && !opt_blankPageUrl) {
  269. if (window.location.protocol == 'https') {
  270. this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(
  271. goog.string.Const.from('https:///'));
  272. } else {
  273. this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(
  274. goog.string.Const.from('javascript:""'));
  275. }
  276. }
  277. /**
  278. * A timer for polling the current history state for changes.
  279. * @type {goog.Timer}
  280. * @private
  281. */
  282. this.timer_ = new goog.Timer(goog.History.PollingType.NORMAL);
  283. this.registerDisposable(this.timer_);
  284. /**
  285. * True if the state tokens are displayed in the address bar, false for hidden
  286. * history states.
  287. * @type {boolean}
  288. * @private
  289. */
  290. this.userVisible_ = !opt_invisible;
  291. /**
  292. * An object to keep track of the history event listeners.
  293. * @type {goog.events.EventHandler<!goog.History>}
  294. * @private
  295. */
  296. this.eventHandler_ = new goog.events.EventHandler(this);
  297. if (opt_invisible || goog.History.LEGACY_IE) {
  298. var iframe;
  299. if (opt_iframe) {
  300. iframe = opt_iframe;
  301. } else {
  302. var iframeId = 'history_iframe' + goog.History.historyCount_;
  303. // Using a "sandbox" attribute on the iframe might be possible, but
  304. // this HTML didn't initially have it and when it was refactored
  305. // to SafeHtml it was kept without it.
  306. var iframeHtml = goog.html.SafeHtml.createIframe(this.iframeSrc_, null, {
  307. id: iframeId,
  308. style: goog.string.Const.from('display:none'),
  309. sandbox: undefined
  310. });
  311. goog.dom.safe.documentWrite(document, iframeHtml);
  312. iframe = goog.dom.getElement(iframeId);
  313. }
  314. /**
  315. * Internet Explorer uses a hidden iframe for all history changes. Other
  316. * browsers use the iframe only for pushing invisible states.
  317. * @type {HTMLIFrameElement}
  318. * @private
  319. */
  320. this.iframe_ = /** @type {HTMLIFrameElement} */ (iframe);
  321. /**
  322. * Whether the hidden iframe has had a document written to it yet in this
  323. * session.
  324. * @type {boolean}
  325. * @private
  326. */
  327. this.unsetIframe_ = true;
  328. }
  329. if (goog.History.LEGACY_IE) {
  330. // IE relies on the hidden input to restore the history state from previous
  331. // sessions, but input values are only restored after window.onload. Set up
  332. // a callback to poll the value after the onload event.
  333. this.eventHandler_.listen(
  334. this.window_, goog.events.EventType.LOAD, this.onDocumentLoaded);
  335. /**
  336. * IE-only variable for determining if the document has loaded.
  337. * @type {boolean}
  338. * @protected
  339. */
  340. this.documentLoaded = false;
  341. /**
  342. * IE-only variable for storing whether the history object should be enabled
  343. * once the document finishes loading.
  344. * @type {boolean}
  345. * @private
  346. */
  347. this.shouldEnable_ = false;
  348. }
  349. // Set the initial history state.
  350. if (this.userVisible_) {
  351. this.setHash_(this.getToken(), true);
  352. } else {
  353. this.setIframeToken_(this.hiddenInput_.value);
  354. }
  355. goog.History.historyCount_++;
  356. };
  357. goog.inherits(goog.History, goog.events.EventTarget);
  358. /**
  359. * Status of when the object is active and dispatching events.
  360. * @type {boolean}
  361. * @private
  362. */
  363. goog.History.prototype.enabled_ = false;
  364. /**
  365. * Whether the object is performing polling with longer intervals. This can
  366. * occur for instance when setting the location of the iframe when in invisible
  367. * mode and the server that is hosting the blank html page is down. In FF, this
  368. * will cause the location of the iframe to no longer be accessible, with
  369. * permision denied exceptions being thrown on every access of the history
  370. * token. When this occurs, the polling interval is elongated. This causes
  371. * exceptions to be thrown at a lesser rate while allowing for the history
  372. * object to resurrect itself when the html page becomes accessible.
  373. * @type {boolean}
  374. * @private
  375. */
  376. goog.History.prototype.longerPolling_ = false;
  377. /**
  378. * The last token set by the history object, used to poll for changes.
  379. * @type {?string}
  380. * @private
  381. */
  382. goog.History.prototype.lastToken_ = null;
  383. /**
  384. * Whether the browser supports HTML5 history management's onhashchange event.
  385. * {@link http://www.w3.org/TR/html5/history.html}. IE 9 in compatibility mode
  386. * indicates that onhashchange is in window, but testing reveals the event
  387. * isn't actually fired.
  388. * @return {boolean} Whether onhashchange is supported.
  389. */
  390. goog.History.isOnHashChangeSupported = goog.memoize(function() {
  391. return goog.userAgent.IE ? goog.userAgent.isDocumentModeOrHigher(8) :
  392. 'onhashchange' in goog.global;
  393. });
  394. /**
  395. * Whether the current browser is Internet Explorer prior to version 8. Many IE
  396. * specific workarounds developed before version 8 are unnecessary in more
  397. * current versions.
  398. * @type {boolean}
  399. */
  400. goog.History.LEGACY_IE =
  401. goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8);
  402. /**
  403. * Whether the browser always requires the hash to be present. Internet Explorer
  404. * before version 8 will reload the HTML page if the hash is omitted.
  405. * @type {boolean}
  406. */
  407. goog.History.HASH_ALWAYS_REQUIRED = goog.History.LEGACY_IE;
  408. /**
  409. * If not null, polling in the user invisible mode will be disabled until this
  410. * token is seen. This is used to prevent a race condition where the iframe
  411. * hangs temporarily while the location is changed.
  412. * @type {?string}
  413. * @private
  414. */
  415. goog.History.prototype.lockedToken_ = null;
  416. /** @override */
  417. goog.History.prototype.disposeInternal = function() {
  418. goog.History.superClass_.disposeInternal.call(this);
  419. this.eventHandler_.dispose();
  420. this.setEnabled(false);
  421. };
  422. /**
  423. * Starts or stops the History polling loop. When enabled, the History object
  424. * will immediately fire an event for the current location. The caller can set
  425. * up event listeners between the call to the constructor and the call to
  426. * setEnabled.
  427. *
  428. * On IE, actual startup may be delayed until the iframe and hidden input
  429. * element have been loaded and can be polled. This behavior is transparent to
  430. * the caller.
  431. *
  432. * @param {boolean} enable Whether to enable the history polling loop.
  433. */
  434. goog.History.prototype.setEnabled = function(enable) {
  435. if (enable == this.enabled_) {
  436. return;
  437. }
  438. if (goog.History.LEGACY_IE && !this.documentLoaded) {
  439. // Wait until the document has actually loaded before enabling the
  440. // object or any saved state from a previous session will be lost.
  441. this.shouldEnable_ = enable;
  442. return;
  443. }
  444. if (enable) {
  445. if (goog.userAgent.OPERA) {
  446. // Capture events for common user input so we can restart the timer in
  447. // Opera if it fails. Yes, this is distasteful. See operaDefibrillator_.
  448. this.eventHandler_.listen(
  449. this.window_.document, goog.History.INPUT_EVENTS_,
  450. this.operaDefibrillator_);
  451. } else if (goog.userAgent.GECKO) {
  452. // Firefox will not restore the correct state after navigating away from
  453. // and then back to the page with the history object. This can be fixed
  454. // by restarting the history object on the pageshow event.
  455. this.eventHandler_.listen(this.window_, 'pageshow', this.onShow_);
  456. }
  457. // TODO(user): make HTML5 and invisible history work by listening to the
  458. // iframe # changes instead of the window.
  459. if (goog.History.isOnHashChangeSupported() && this.userVisible_) {
  460. this.eventHandler_.listen(
  461. this.window_, goog.events.EventType.HASHCHANGE, this.onHashChange_);
  462. this.enabled_ = true;
  463. this.dispatchEvent(new goog.history.Event(this.getToken(), false));
  464. } else if (
  465. !(goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) ||
  466. this.documentLoaded) {
  467. // Start dispatching history events if all necessary loading has
  468. // completed (always true for browsers other than IE.)
  469. this.eventHandler_.listen(
  470. this.timer_, goog.Timer.TICK, goog.bind(this.check_, this, true));
  471. this.enabled_ = true;
  472. // Initialize last token at startup except on IE < 8, where the last token
  473. // must only be set in conjunction with IFRAME updates, or the IFRAME will
  474. // start out of sync and remove any pre-existing URI fragment.
  475. if (!goog.History.LEGACY_IE) {
  476. this.lastToken_ = this.getToken();
  477. this.dispatchEvent(new goog.history.Event(this.getToken(), false));
  478. }
  479. this.timer_.start();
  480. }
  481. } else {
  482. this.enabled_ = false;
  483. this.eventHandler_.removeAll();
  484. this.timer_.stop();
  485. }
  486. };
  487. /**
  488. * Callback for the window onload event in IE. This is necessary to read the
  489. * value of the hidden input after restoring a history session. The value of
  490. * input elements is not viewable until after window onload for some reason (the
  491. * iframe state is similarly unavailable during the loading phase.) If
  492. * setEnabled is called before the iframe has completed loading, the history
  493. * object will actually be enabled at this point.
  494. * @protected
  495. */
  496. goog.History.prototype.onDocumentLoaded = function() {
  497. this.documentLoaded = true;
  498. if (this.hiddenInput_.value) {
  499. // Any saved value in the hidden input can only be read after the document
  500. // has been loaded due to an IE limitation. Restore the previous state if
  501. // it has been set.
  502. this.setIframeToken_(this.hiddenInput_.value, true);
  503. }
  504. this.setEnabled(this.shouldEnable_);
  505. };
  506. /**
  507. * Handler for the Gecko pageshow event. Restarts the history object so that the
  508. * correct state can be restored in the hash or iframe.
  509. * @param {goog.events.BrowserEvent} e The browser event.
  510. * @private
  511. */
  512. goog.History.prototype.onShow_ = function(e) {
  513. // NOTE(user): persisted is a property passed in the pageshow event that
  514. // indicates whether the page is being persisted from the cache or is being
  515. // loaded for the first time.
  516. if (e.getBrowserEvent()['persisted']) {
  517. this.setEnabled(false);
  518. this.setEnabled(true);
  519. }
  520. };
  521. /**
  522. * Handles HTML5 onhashchange events on browsers where it is supported.
  523. * This is very similar to {@link #check_}, except that it is not executed
  524. * continuously. It is only used when
  525. * {@code goog.History.isOnHashChangeSupported()} is true.
  526. * @param {goog.events.BrowserEvent} e The browser event.
  527. * @private
  528. */
  529. goog.History.prototype.onHashChange_ = function(e) {
  530. var hash = this.getLocationFragment_(this.window_);
  531. if (hash != this.lastToken_) {
  532. this.update_(hash, true);
  533. }
  534. };
  535. /**
  536. * @return {string} The current token.
  537. */
  538. goog.History.prototype.getToken = function() {
  539. if (this.lockedToken_ != null) {
  540. return this.lockedToken_;
  541. } else if (this.userVisible_) {
  542. return this.getLocationFragment_(this.window_);
  543. } else {
  544. return this.getIframeToken_() || '';
  545. }
  546. };
  547. /**
  548. * Sets the history state. When user visible states are used, the URL fragment
  549. * will be set to the provided token. Sometimes it is necessary to set the
  550. * history token before the document title has changed, in this case IE's
  551. * history drop down can be out of sync with the token. To get around this
  552. * problem, the app can pass in a title to use with the hidden iframe.
  553. * @param {string} token The history state identifier.
  554. * @param {string=} opt_title Optional title used when setting the hidden iframe
  555. * title in IE.
  556. */
  557. goog.History.prototype.setToken = function(token, opt_title) {
  558. this.setHistoryState_(token, false, opt_title);
  559. };
  560. /**
  561. * Replaces the current history state without affecting the rest of the history
  562. * stack.
  563. * @param {string} token The history state identifier.
  564. * @param {string=} opt_title Optional title used when setting the hidden iframe
  565. * title in IE.
  566. */
  567. goog.History.prototype.replaceToken = function(token, opt_title) {
  568. this.setHistoryState_(token, true, opt_title);
  569. };
  570. /**
  571. * Gets the location fragment for the current URL. We don't use location.hash
  572. * directly as the browser helpfully urlDecodes the string for us which can
  573. * corrupt the tokens. For example, if we want to store: label/%2Froot it would
  574. * be returned as label//root.
  575. * @param {Window} win The window object to use.
  576. * @return {string} The fragment.
  577. * @private
  578. */
  579. goog.History.prototype.getLocationFragment_ = function(win) {
  580. var href = win.location.href;
  581. var index = href.indexOf('#');
  582. return index < 0 ? '' : href.substring(index + 1);
  583. };
  584. /**
  585. * Sets the history state. When user visible states are used, the URL fragment
  586. * will be set to the provided token. Setting opt_replace to true will cause the
  587. * navigation to occur, but will replace the current history entry without
  588. * affecting the length of the stack.
  589. *
  590. * @param {string} token The history state identifier.
  591. * @param {boolean} replace Set to replace the current history entry instead of
  592. * appending a new history state.
  593. * @param {string=} opt_title Optional title used when setting the hidden iframe
  594. * title in IE.
  595. * @private
  596. */
  597. goog.History.prototype.setHistoryState_ = function(token, replace, opt_title) {
  598. if (this.getToken() != token) {
  599. if (this.userVisible_) {
  600. this.setHash_(token, replace);
  601. if (!goog.History.isOnHashChangeSupported()) {
  602. if (goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) {
  603. // IE must save state using the iframe.
  604. this.setIframeToken_(token, replace, opt_title);
  605. }
  606. }
  607. // This condition needs to be called even if
  608. // goog.History.isOnHashChangeSupported() is true so the NAVIGATE event
  609. // fires sychronously.
  610. if (this.enabled_) {
  611. this.check_(false);
  612. }
  613. } else {
  614. // Fire the event immediately so that setting history is synchronous, but
  615. // set a suspendToken so that polling doesn't trigger a 'back'.
  616. this.setIframeToken_(token, replace);
  617. this.lockedToken_ = this.lastToken_ = this.hiddenInput_.value = token;
  618. this.dispatchEvent(new goog.history.Event(token, false));
  619. }
  620. }
  621. };
  622. /**
  623. * Sets or replaces the URL fragment. The token does not need to be URL encoded
  624. * according to the URL specification, though certain characters (like newline)
  625. * are automatically stripped.
  626. *
  627. * If opt_replace is not set, non-IE browsers will append a new entry to the
  628. * history list. Setting the hash does not affect the history stack in IE
  629. * (unless there is a pre-existing named anchor for that hash.)
  630. *
  631. * Older versions of Webkit cannot query the location hash, but it still can be
  632. * set. If we detect one of these versions, always replace instead of creating
  633. * new history entries.
  634. *
  635. * window.location.replace replaces the current state from the history stack.
  636. * http://www.whatwg.org/specs/web-apps/current-work/#dom-location-replace
  637. * http://www.whatwg.org/specs/web-apps/current-work/#replacement-enabled
  638. *
  639. * @param {string} token The new string to set.
  640. * @param {boolean=} opt_replace Set to true to replace the current token
  641. * without appending a history entry.
  642. * @private
  643. */
  644. goog.History.prototype.setHash_ = function(token, opt_replace) {
  645. // If the page uses a BASE element, setting location.hash directly will
  646. // navigate away from the current document. Also, the original URL path may
  647. // possibly change from HTML5 history pushState. To account for these, the
  648. // full path is always specified.
  649. var loc = this.window_.location;
  650. var url = loc.href.split('#')[0];
  651. // If a hash has already been set, then removing it programmatically will
  652. // reload the page. Once there is a hash, we won't remove it.
  653. var hasHash = goog.string.contains(loc.href, '#');
  654. if (goog.History.HASH_ALWAYS_REQUIRED || hasHash || token) {
  655. url += '#' + token;
  656. }
  657. if (url != loc.href) {
  658. if (opt_replace) {
  659. loc.replace(url);
  660. } else {
  661. loc.href = url;
  662. }
  663. }
  664. };
  665. /**
  666. * Sets the hidden iframe state. On IE, this is accomplished by writing a new
  667. * document into the iframe. In Firefox, the iframe's URL fragment stores the
  668. * state instead.
  669. *
  670. * Older versions of webkit cannot set the iframe, so ignore those browsers.
  671. *
  672. * @param {string} token The new string to set.
  673. * @param {boolean=} opt_replace Set to true to replace the current iframe state
  674. * without appending a new history entry.
  675. * @param {string=} opt_title Optional title used when setting the hidden iframe
  676. * title in IE.
  677. * @private
  678. */
  679. goog.History.prototype.setIframeToken_ = function(
  680. token, opt_replace, opt_title) {
  681. if (this.unsetIframe_ || token != this.getIframeToken_()) {
  682. this.unsetIframe_ = false;
  683. token = goog.string.urlEncode(token);
  684. if (goog.userAgent.IE) {
  685. // Caching the iframe document results in document permission errors after
  686. // leaving the page and returning. Access it anew each time instead.
  687. var doc = goog.dom.getFrameContentDocument(this.iframe_);
  688. doc.open('text/html', opt_replace ? 'replace' : undefined);
  689. var iframeSourceHtml = goog.html.SafeHtml.concat(
  690. goog.html.SafeHtml.create(
  691. 'title', {}, (opt_title || this.window_.document.title)),
  692. goog.html.SafeHtml.create('body', {}, token));
  693. goog.dom.safe.documentWrite(doc, iframeSourceHtml);
  694. doc.close();
  695. } else {
  696. goog.asserts.assertInstanceof(
  697. this.iframeSrc_, goog.html.TrustedResourceUrl,
  698. 'this.iframeSrc_ must be set on calls to setIframeToken_');
  699. var url =
  700. goog.html.TrustedResourceUrl.unwrap(
  701. /** @type {!goog.html.TrustedResourceUrl} */ (this.iframeSrc_)) +
  702. '#' + token;
  703. // In Safari, it is possible for the contentWindow of the iframe to not
  704. // be present when the page is loading after a reload.
  705. var contentWindow = this.iframe_.contentWindow;
  706. if (contentWindow) {
  707. if (opt_replace) {
  708. contentWindow.location.replace(url);
  709. } else {
  710. contentWindow.location.href = url;
  711. }
  712. }
  713. }
  714. }
  715. };
  716. /**
  717. * Return the current state string from the hidden iframe. On internet explorer,
  718. * this is stored as a string in the document body. Other browsers use the
  719. * location hash of the hidden iframe.
  720. *
  721. * Older versions of webkit cannot access the iframe location, so always return
  722. * null in that case.
  723. *
  724. * @return {?string} The state token saved in the iframe (possibly null if the
  725. * iframe has never loaded.).
  726. * @private
  727. */
  728. goog.History.prototype.getIframeToken_ = function() {
  729. if (goog.userAgent.IE) {
  730. var doc = goog.dom.getFrameContentDocument(this.iframe_);
  731. return doc.body ? goog.string.urlDecode(doc.body.innerHTML) : null;
  732. } else {
  733. // In Safari, it is possible for the contentWindow of the iframe to not
  734. // be present when the page is loading after a reload.
  735. var contentWindow = this.iframe_.contentWindow;
  736. if (contentWindow) {
  737. var hash;
  738. try {
  739. // Iframe tokens are urlEncoded
  740. hash = goog.string.urlDecode(this.getLocationFragment_(contentWindow));
  741. } catch (e) {
  742. // An exception will be thrown if the location of the iframe can not be
  743. // accessed (permission denied). This can occur in FF if the the server
  744. // that is hosting the blank html page goes down and then a new history
  745. // token is set. The iframe will navigate to an error page, and the
  746. // location of the iframe can no longer be accessed. Due to the polling,
  747. // this will cause constant exceptions to be thrown. In this case,
  748. // we enable longer polling. We do not have to attempt to reset the
  749. // iframe token because (a) we already fired the NAVIGATE event when
  750. // setting the token, (b) we can rely on the locked token for current
  751. // state, and (c) the token is still in the history and
  752. // accesible on forward/back.
  753. if (!this.longerPolling_) {
  754. this.setLongerPolling_(true);
  755. }
  756. return null;
  757. }
  758. // There was no exception when getting the hash so turn off longer polling
  759. // if it is on.
  760. if (this.longerPolling_) {
  761. this.setLongerPolling_(false);
  762. }
  763. return hash || null;
  764. } else {
  765. return null;
  766. }
  767. }
  768. };
  769. /**
  770. * Checks the state of the document fragment and the iframe title to detect
  771. * navigation changes. If {@code goog.HistoryisOnHashChangeSupported()} is
  772. * {@code false}, then this runs approximately twenty times per second.
  773. * @param {boolean} isNavigation True if the event was initiated by a browser
  774. * action, false if it was caused by a setToken call. See
  775. * {@link goog.history.Event}.
  776. * @private
  777. */
  778. goog.History.prototype.check_ = function(isNavigation) {
  779. if (this.userVisible_) {
  780. var hash = this.getLocationFragment_(this.window_);
  781. if (hash != this.lastToken_) {
  782. this.update_(hash, isNavigation);
  783. }
  784. }
  785. // Old IE uses the iframe for both visible and non-visible versions.
  786. if (!this.userVisible_ || goog.History.LEGACY_IE) {
  787. var token = this.getIframeToken_() || '';
  788. if (this.lockedToken_ == null || token == this.lockedToken_) {
  789. this.lockedToken_ = null;
  790. if (token != this.lastToken_) {
  791. this.update_(token, isNavigation);
  792. }
  793. }
  794. }
  795. };
  796. /**
  797. * Updates the current history state with a given token. Called after a change
  798. * to the location or the iframe state is detected by poll_.
  799. *
  800. * @param {string} token The new history state.
  801. * @param {boolean} isNavigation True if the event was initiated by a browser
  802. * action, false if it was caused by a setToken call. See
  803. * {@link goog.history.Event}.
  804. * @private
  805. */
  806. goog.History.prototype.update_ = function(token, isNavigation) {
  807. this.lastToken_ = this.hiddenInput_.value = token;
  808. if (this.userVisible_) {
  809. if (goog.History.LEGACY_IE) {
  810. this.setIframeToken_(token);
  811. }
  812. this.setHash_(token);
  813. } else {
  814. this.setIframeToken_(token);
  815. }
  816. this.dispatchEvent(new goog.history.Event(this.getToken(), isNavigation));
  817. };
  818. /**
  819. * Sets if the history oject should use longer intervals when polling.
  820. *
  821. * @param {boolean} longerPolling Whether to enable longer polling.
  822. * @private
  823. */
  824. goog.History.prototype.setLongerPolling_ = function(longerPolling) {
  825. if (this.longerPolling_ != longerPolling) {
  826. this.timer_.setInterval(
  827. longerPolling ? goog.History.PollingType.LONG :
  828. goog.History.PollingType.NORMAL);
  829. }
  830. this.longerPolling_ = longerPolling;
  831. };
  832. /**
  833. * Opera cancels all outstanding timeouts and intervals after any rapid
  834. * succession of navigation events, including the interval used to detect
  835. * navigation events. This function restarts the interval so that navigation can
  836. * continue. Ideally, only events which would be likely to cause a navigation
  837. * change (mousedown and keydown) would be bound to this function. Since Opera
  838. * seems to ignore keydown events while the alt key is pressed (such as
  839. * alt-left or right arrow), this function is also bound to the much more
  840. * frequent mousemove event. This way, when the update loop freezes, it will
  841. * unstick itself as the user wiggles the mouse in frustration.
  842. * @private
  843. */
  844. goog.History.prototype.operaDefibrillator_ = function() {
  845. this.timer_.stop();
  846. this.timer_.start();
  847. };
  848. /**
  849. * List of user input event types registered in Opera to restart the history
  850. * timer (@see goog.History#operaDefibrillator_).
  851. * @type {Array<string>}
  852. * @private
  853. */
  854. goog.History.INPUT_EVENTS_ = [
  855. goog.events.EventType.MOUSEDOWN, goog.events.EventType.KEYDOWN,
  856. goog.events.EventType.MOUSEMOVE
  857. ];
  858. /**
  859. * Counter for the number of goog.History objects that have been instantiated.
  860. * Used to create unique IDs.
  861. * @type {number}
  862. * @private
  863. */
  864. goog.History.historyCount_ = 0;
  865. /**
  866. * Types of polling. The values are in ms of the polling interval.
  867. * @enum {number}
  868. */
  869. goog.History.PollingType = {
  870. NORMAL: 150,
  871. LONG: 10000
  872. };
  873. /**
  874. * Constant for the history change event type.
  875. * @enum {string}
  876. * @deprecated Use goog.history.EventType.
  877. */
  878. goog.History.EventType = goog.history.EventType;
  879. /**
  880. * Constant for the history change event type.
  881. * @constructor
  882. * @deprecated Use goog.history.Event.
  883. * @final
  884. */
  885. goog.History.Event = goog.history.Event;