html5history.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // Copyright 2010 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 HTML5 based history implementation, compatible with
  16. * goog.History.
  17. *
  18. * TODO(user): There should really be a history interface and multiple
  19. * implementations.
  20. *
  21. */
  22. goog.provide('goog.history.Html5History');
  23. goog.provide('goog.history.Html5History.TokenTransformer');
  24. goog.require('goog.asserts');
  25. goog.require('goog.events');
  26. goog.require('goog.events.EventTarget');
  27. goog.require('goog.events.EventType');
  28. goog.require('goog.history.Event');
  29. /**
  30. * An implementation compatible with goog.History that uses the HTML5
  31. * history APIs.
  32. *
  33. * @param {Window=} opt_win The window to listen/dispatch history events on.
  34. * @param {goog.history.Html5History.TokenTransformer=} opt_transformer
  35. * The token transformer that is used to create URL from the token
  36. * when storing token without using hash fragment.
  37. * @constructor
  38. * @extends {goog.events.EventTarget}
  39. * @final
  40. */
  41. goog.history.Html5History = function(opt_win, opt_transformer) {
  42. goog.events.EventTarget.call(this);
  43. goog.asserts.assert(
  44. goog.history.Html5History.isSupported(opt_win),
  45. 'HTML5 history is not supported.');
  46. /**
  47. * The window object to use for history tokens. Typically the top window.
  48. * @type {Window}
  49. * @private
  50. */
  51. this.window_ = opt_win || window;
  52. /**
  53. * The token transformer that is used to create URL from the token
  54. * when storing token without using hash fragment.
  55. * @type {goog.history.Html5History.TokenTransformer}
  56. * @private
  57. */
  58. this.transformer_ = opt_transformer || null;
  59. /**
  60. * The fragment of the last navigation. Used to eliminate duplicate/redundant
  61. * NAVIGATE events when a POPSTATE and HASHCHANGE event are triggered for the
  62. * same navigation (e.g., back button click).
  63. * @private {?string}
  64. */
  65. this.lastFragment_ = null;
  66. goog.events.listen(
  67. this.window_, goog.events.EventType.POPSTATE, this.onHistoryEvent_, false,
  68. this);
  69. goog.events.listen(
  70. this.window_, goog.events.EventType.HASHCHANGE, this.onHistoryEvent_,
  71. false, this);
  72. };
  73. goog.inherits(goog.history.Html5History, goog.events.EventTarget);
  74. /**
  75. * Returns whether Html5History is supported.
  76. * @param {Window=} opt_win Optional window to check.
  77. * @return {boolean} Whether html5 history is supported.
  78. */
  79. goog.history.Html5History.isSupported = function(opt_win) {
  80. var win = opt_win || window;
  81. return !!(win.history && win.history.pushState);
  82. };
  83. /**
  84. * Status of when the object is active and dispatching events.
  85. * @type {boolean}
  86. * @private
  87. */
  88. goog.history.Html5History.prototype.enabled_ = false;
  89. /**
  90. * Whether to use the fragment to store the token, defaults to true.
  91. * @type {boolean}
  92. * @private
  93. */
  94. goog.history.Html5History.prototype.useFragment_ = true;
  95. /**
  96. * If useFragment is false the path will be used, the path prefix will be
  97. * prepended to all tokens. Defaults to '/'.
  98. * @type {string}
  99. * @private
  100. */
  101. goog.history.Html5History.prototype.pathPrefix_ = '/';
  102. /**
  103. * Starts or stops the History. When enabled, the History object
  104. * will immediately fire an event for the current location. The caller can set
  105. * up event listeners between the call to the constructor and the call to
  106. * setEnabled.
  107. *
  108. * @param {boolean} enable Whether to enable history.
  109. */
  110. goog.history.Html5History.prototype.setEnabled = function(enable) {
  111. if (enable == this.enabled_) {
  112. return;
  113. }
  114. this.enabled_ = enable;
  115. if (enable) {
  116. this.dispatchEvent(new goog.history.Event(this.getToken(), false));
  117. }
  118. };
  119. /**
  120. * Returns the current token.
  121. * @return {string} The current token.
  122. */
  123. goog.history.Html5History.prototype.getToken = function() {
  124. if (this.useFragment_) {
  125. return goog.asserts.assertString(this.getFragment_());
  126. } else {
  127. return this.transformer_ ?
  128. this.transformer_.retrieveToken(
  129. this.pathPrefix_, this.window_.location) :
  130. this.window_.location.pathname.substr(this.pathPrefix_.length);
  131. }
  132. };
  133. /**
  134. * Sets the history state.
  135. * @param {string} token The history state identifier.
  136. * @param {string=} opt_title Optional title to associate with history entry.
  137. */
  138. goog.history.Html5History.prototype.setToken = function(token, opt_title) {
  139. if (token == this.getToken()) {
  140. return;
  141. }
  142. // Per externs/gecko_dom.js document.title can be null.
  143. this.window_.history.pushState(
  144. null, opt_title || this.window_.document.title || '',
  145. this.getUrl_(token));
  146. this.dispatchEvent(new goog.history.Event(token, false));
  147. };
  148. /**
  149. * Replaces the current history state without affecting the rest of the history
  150. * stack.
  151. * @param {string} token The history state identifier.
  152. * @param {string=} opt_title Optional title to associate with history entry.
  153. */
  154. goog.history.Html5History.prototype.replaceToken = function(token, opt_title) {
  155. // Per externs/gecko_dom.js document.title can be null.
  156. this.window_.history.replaceState(
  157. null, opt_title || this.window_.document.title || '',
  158. this.getUrl_(token));
  159. this.dispatchEvent(new goog.history.Event(token, false));
  160. };
  161. /** @override */
  162. goog.history.Html5History.prototype.disposeInternal = function() {
  163. goog.events.unlisten(
  164. this.window_, goog.events.EventType.POPSTATE, this.onHistoryEvent_, false,
  165. this);
  166. if (this.useFragment_) {
  167. goog.events.unlisten(
  168. this.window_, goog.events.EventType.HASHCHANGE, this.onHistoryEvent_,
  169. false, this);
  170. }
  171. };
  172. /**
  173. * Sets whether to use the fragment to store tokens.
  174. * @param {boolean} useFragment Whether to use the fragment.
  175. */
  176. goog.history.Html5History.prototype.setUseFragment = function(useFragment) {
  177. if (this.useFragment_ != useFragment) {
  178. if (useFragment) {
  179. goog.events.listen(
  180. this.window_, goog.events.EventType.HASHCHANGE, this.onHistoryEvent_,
  181. false, this);
  182. } else {
  183. goog.events.unlisten(
  184. this.window_, goog.events.EventType.HASHCHANGE, this.onHistoryEvent_,
  185. false, this);
  186. }
  187. this.useFragment_ = useFragment;
  188. }
  189. };
  190. /**
  191. * Sets the path prefix to use if storing tokens in the path. The path
  192. * prefix should start and end with slash.
  193. * @param {string} pathPrefix Sets the path prefix.
  194. */
  195. goog.history.Html5History.prototype.setPathPrefix = function(pathPrefix) {
  196. this.pathPrefix_ = pathPrefix;
  197. };
  198. /**
  199. * Gets the path prefix.
  200. * @return {string} The path prefix.
  201. */
  202. goog.history.Html5History.prototype.getPathPrefix = function() {
  203. return this.pathPrefix_;
  204. };
  205. /**
  206. * Gets the current hash fragment, if useFragment_ is enabled.
  207. * @return {?string} The hash fragment.
  208. * @private
  209. */
  210. goog.history.Html5History.prototype.getFragment_ = function() {
  211. if (this.useFragment_) {
  212. var loc = this.window_.location.href;
  213. var index = loc.indexOf('#');
  214. return index < 0 ? '' : loc.substring(index + 1);
  215. } else {
  216. return null;
  217. }
  218. };
  219. /**
  220. * Gets the URL to set when calling history.pushState
  221. * @param {string} token The history token.
  222. * @return {string} The URL.
  223. * @private
  224. */
  225. goog.history.Html5History.prototype.getUrl_ = function(token) {
  226. if (this.useFragment_) {
  227. return '#' + token;
  228. } else {
  229. return this.transformer_ ?
  230. this.transformer_.createUrl(
  231. token, this.pathPrefix_, this.window_.location) :
  232. this.pathPrefix_ + token + this.window_.location.search;
  233. }
  234. };
  235. /**
  236. * Handles history events dispatched by the browser.
  237. * @param {goog.events.BrowserEvent} e The browser event object.
  238. * @private
  239. */
  240. goog.history.Html5History.prototype.onHistoryEvent_ = function(e) {
  241. if (this.enabled_) {
  242. var fragment = this.getFragment_();
  243. // Only fire NAVIGATE event if it's POPSTATE or if the fragment has changed
  244. // without a POPSTATE event. The latter is an indication the browser doesn't
  245. // support POPSTATE, and the event is a HASHCHANGE instead.
  246. if (e.type == goog.events.EventType.POPSTATE ||
  247. fragment != this.lastFragment_) {
  248. this.lastFragment_ = fragment;
  249. this.dispatchEvent(new goog.history.Event(this.getToken(), true));
  250. }
  251. }
  252. };
  253. /**
  254. * A token transformer that can create a URL from a history
  255. * token. This is used by {@code goog.history.Html5History} to create
  256. * URL when storing token without the hash fragment.
  257. *
  258. * Given a {@code window.location} object containing the location
  259. * created by {@code createUrl}, the token transformer allows
  260. * retrieval of the token back via {@code retrieveToken}.
  261. *
  262. * @interface
  263. */
  264. goog.history.Html5History.TokenTransformer = function() {};
  265. /**
  266. * Retrieves a history token given the path prefix and
  267. * {@code window.location} object.
  268. *
  269. * @param {string} pathPrefix The path prefix to use when storing token
  270. * in a path; always begin with a slash.
  271. * @param {Location} location The {@code window.location} object.
  272. * Treat this object as read-only.
  273. * @return {string} token The history token.
  274. */
  275. goog.history.Html5History.TokenTransformer.prototype.retrieveToken = function(
  276. pathPrefix, location) {};
  277. /**
  278. * Creates a URL to be pushed into HTML5 history stack when storing
  279. * token without using hash fragment.
  280. *
  281. * @param {string} token The history token.
  282. * @param {string} pathPrefix The path prefix to use when storing token
  283. * in a path; always begin with a slash.
  284. * @param {Location} location The {@code window.location} object.
  285. * Treat this object as read-only.
  286. * @return {string} url The complete URL string from path onwards
  287. * (without {@code protocol://host:port} part); must begin with a
  288. * slash.
  289. */
  290. goog.history.Html5History.TokenTransformer.prototype.createUrl = function(
  291. token, pathPrefix, location) {};