Version 3.18.1
Show:

File: history/js/history-hash.js

  1. /**
  2. * Provides browser history management backed by
  3. * <code>window.location.hash</code>, as well as convenience methods for working
  4. * with the location hash and a synthetic <code>hashchange</code> event that
  5. * normalizes differences across browsers.
  6. *
  7. * @module history
  8. * @submodule history-hash
  9. * @since 3.2.0
  10. * @class HistoryHash
  11. * @extends HistoryBase
  12. * @constructor
  13. * @param {Object} config (optional) Configuration object. See the HistoryBase
  14. * documentation for details.
  15. */
  16. var HistoryBase = Y.HistoryBase,
  17. Lang = Y.Lang,
  18. YArray = Y.Array,
  19. YObject = Y.Object,
  20. GlobalEnv = YUI.namespace('Env.HistoryHash'),
  21. SRC_HASH = 'hash',
  22. hashNotifiers,
  23. oldHash,
  24. oldUrl,
  25. win = Y.config.win,
  26. useHistoryHTML5 = Y.config.useHistoryHTML5;
  27. function HistoryHash() {
  28. HistoryHash.superclass.constructor.apply(this, arguments);
  29. }
  30. Y.extend(HistoryHash, HistoryBase, {
  31. // -- Initialization -------------------------------------------------------
  32. _init: function (config) {
  33. var bookmarkedState = HistoryHash.parseHash();
  34. // If an initialState was provided, merge the bookmarked state into it
  35. // (the bookmarked state wins).
  36. config = config || {};
  37. this._initialState = config.initialState ?
  38. Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
  39. // Subscribe to the synthetic hashchange event (defined below) to handle
  40. // changes.
  41. Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
  42. HistoryHash.superclass._init.apply(this, arguments);
  43. },
  44. // -- Protected Methods ----------------------------------------------------
  45. _change: function (src, state, options) {
  46. // Stringify all values to ensure that comparisons don't fail after
  47. // they're coerced to strings in the location hash.
  48. YObject.each(state, function (value, key) {
  49. if (Lang.isValue(value)) {
  50. state[key] = value.toString();
  51. }
  52. });
  53. return HistoryHash.superclass._change.call(this, src, state, options);
  54. },
  55. _storeState: function (src, newState) {
  56. var decode = HistoryHash.decode,
  57. newHash = HistoryHash.createHash(newState);
  58. HistoryHash.superclass._storeState.apply(this, arguments);
  59. // Update the location hash with the changes, but only if the new hash
  60. // actually differs from the current hash (this avoids creating multiple
  61. // history entries for a single state).
  62. //
  63. // We always compare decoded hashes, since it's possible that the hash
  64. // could be set incorrectly to a non-encoded value outside of
  65. // HistoryHash.
  66. if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(newHash)) {
  67. HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
  68. }
  69. },
  70. // -- Protected Event Handlers ---------------------------------------------
  71. /**
  72. * Handler for hashchange events.
  73. *
  74. * @method _afterHashChange
  75. * @param {Event} e
  76. * @protected
  77. */
  78. _afterHashChange: function (e) {
  79. this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
  80. }
  81. }, {
  82. // -- Public Static Properties ---------------------------------------------
  83. NAME: 'historyHash',
  84. /**
  85. * Constant used to identify state changes originating from
  86. * <code>hashchange</code> events.
  87. *
  88. * @property SRC_HASH
  89. * @type String
  90. * @static
  91. * @final
  92. */
  93. SRC_HASH: SRC_HASH,
  94. /**
  95. * <p>
  96. * Prefix to prepend when setting the hash fragment. For example, if the
  97. * prefix is <code>!</code> and the hash fragment is set to
  98. * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
  99. * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
  100. * Ajax application crawlable in accordance with Google's guidelines at
  101. * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
  102. * </p>
  103. *
  104. * <p>
  105. * Note that this prefix applies to all HistoryHash instances. It's not
  106. * possible for individual instances to use their own prefixes since they
  107. * all operate on the same URL.
  108. * </p>
  109. *
  110. * @property hashPrefix
  111. * @type String
  112. * @default ''
  113. * @static
  114. */
  115. hashPrefix: '',
  116. // -- Protected Static Properties ------------------------------------------
  117. /**
  118. * Regular expression used to parse location hash/query strings.
  119. *
  120. * @property _REGEX_HASH
  121. * @type RegExp
  122. * @protected
  123. * @static
  124. * @final
  125. */
  126. _REGEX_HASH: /([^\?#&=]+)=?([^&=]*)/g,
  127. // -- Public Static Methods ------------------------------------------------
  128. /**
  129. * Creates a location hash string from the specified object of key/value
  130. * pairs.
  131. *
  132. * @method createHash
  133. * @param {Object} params object of key/value parameter pairs
  134. * @return {String} location hash string
  135. * @static
  136. */
  137. createHash: function (params) {
  138. var encode = HistoryHash.encode,
  139. hash = [];
  140. YObject.each(params, function (value, key) {
  141. if (Lang.isValue(value)) {
  142. hash.push(encode(key) + '=' + encode(value));
  143. }
  144. });
  145. return hash.join('&');
  146. },
  147. /**
  148. * Wrapper around <code>decodeURIComponent()</code> that also converts +
  149. * chars into spaces.
  150. *
  151. * @method decode
  152. * @param {String} string string to decode
  153. * @return {String} decoded string
  154. * @static
  155. */
  156. decode: function (string) {
  157. return decodeURIComponent(string.replace(/\+/g, ' '));
  158. },
  159. /**
  160. * Wrapper around <code>encodeURIComponent()</code> that converts spaces to
  161. * + chars.
  162. *
  163. * @method encode
  164. * @param {String} string string to encode
  165. * @return {String} encoded string
  166. * @static
  167. */
  168. encode: function (string) {
  169. return encodeURIComponent(string).replace(/%20/g, '+');
  170. },
  171. /**
  172. * Gets the raw (not decoded) current location hash, minus the preceding '#'
  173. * character and the hashPrefix (if one is set).
  174. *
  175. * @method getHash
  176. * @return {String} current location hash
  177. * @static
  178. */
  179. getHash: (Y.UA.gecko ? function () {
  180. // Gecko's window.location.hash returns a decoded string and we want all
  181. // encoding untouched, so we need to get the hash value from
  182. // window.location.href instead. We have to use UA sniffing rather than
  183. // feature detection, since the only way to detect this would be to
  184. // actually change the hash.
  185. var location = Y.getLocation(),
  186. matches = /#(.*)$/.exec(location.href),
  187. hash = matches && matches[1] || '',
  188. prefix = HistoryHash.hashPrefix;
  189. return prefix && hash.indexOf(prefix) === 0 ?
  190. hash.replace(prefix, '') : hash;
  191. } : function () {
  192. var location = Y.getLocation(),
  193. hash = location.hash.substring(1),
  194. prefix = HistoryHash.hashPrefix;
  195. // Slight code duplication here, but execution speed is of the essence
  196. // since getHash() is called every 50ms to poll for changes in browsers
  197. // that don't support native onhashchange. An additional function call
  198. // would add unnecessary overhead.
  199. return prefix && hash.indexOf(prefix) === 0 ?
  200. hash.replace(prefix, '') : hash;
  201. }),
  202. /**
  203. * Gets the current bookmarkable URL.
  204. *
  205. * @method getUrl
  206. * @return {String} current bookmarkable URL
  207. * @static
  208. */
  209. getUrl: function () {
  210. return location.href;
  211. },
  212. /**
  213. * Parses a location hash string into an object of key/value parameter
  214. * pairs. If <i>hash</i> is not specified, the current location hash will
  215. * be used.
  216. *
  217. * @method parseHash
  218. * @param {String} hash (optional) location hash string
  219. * @return {Object} object of parsed key/value parameter pairs
  220. * @static
  221. */
  222. parseHash: function (hash) {
  223. var decode = HistoryHash.decode,
  224. i,
  225. len,
  226. match,
  227. matches,
  228. param,
  229. params = {},
  230. prefix = HistoryHash.hashPrefix,
  231. prefixIndex;
  232. hash = Lang.isValue(hash) ? hash : HistoryHash.getHash();
  233. if (prefix) {
  234. prefixIndex = hash.indexOf(prefix);
  235. if (prefixIndex === 0 || (prefixIndex === 1 && hash.charAt(0) === '#')) {
  236. hash = hash.replace(prefix, '');
  237. }
  238. }
  239. matches = hash.match(HistoryHash._REGEX_HASH) || [];
  240. for (i = 0, len = matches.length; i < len; ++i) {
  241. match = matches[i];
  242. param = match.split('=');
  243. if (param.length > 1) {
  244. params[decode(param[0])] = decode(param[1]);
  245. } else {
  246. params[decode(match)] = '';
  247. }
  248. }
  249. return params;
  250. },
  251. /**
  252. * Replaces the browser's current location hash with the specified hash
  253. * and removes all forward navigation states, without creating a new browser
  254. * history entry. Automatically prepends the <code>hashPrefix</code> if one
  255. * is set.
  256. *
  257. * @method replaceHash
  258. * @param {String} hash new location hash
  259. * @static
  260. */
  261. replaceHash: function (hash) {
  262. var location = Y.getLocation(),
  263. base = location.href.replace(/#.*$/, '');
  264. if (hash.charAt(0) === '#') {
  265. hash = hash.substring(1);
  266. }
  267. location.replace(base + '#' + (HistoryHash.hashPrefix || '') + hash);
  268. },
  269. /**
  270. * Sets the browser's location hash to the specified string. Automatically
  271. * prepends the <code>hashPrefix</code> if one is set.
  272. *
  273. * @method setHash
  274. * @param {String} hash new location hash
  275. * @static
  276. */
  277. setHash: function (hash) {
  278. var location = Y.getLocation();
  279. if (hash.charAt(0) === '#') {
  280. hash = hash.substring(1);
  281. }
  282. location.hash = (HistoryHash.hashPrefix || '') + hash;
  283. }
  284. });
  285. // -- Synthetic hashchange Event -----------------------------------------------
  286. // TODO: YUIDoc currently doesn't provide a good way to document synthetic DOM
  287. // events. For now, we're just documenting the hashchange event on the YUI
  288. // object, which is about the best we can do until enhancements are made to
  289. // YUIDoc.
  290. /**
  291. Synthetic <code>window.onhashchange</code> event that normalizes differences
  292. across browsers and provides support for browsers that don't natively support
  293. <code>onhashchange</code>.
  294. This event is provided by the <code>history-hash</code> module.
  295. @example
  296. YUI().use('history-hash', function (Y) {
  297. Y.on('hashchange', function (e) {
  298. // Handle hashchange events on the current window.
  299. }, Y.config.win);
  300. });
  301. @event hashchange
  302. @param {EventFacade} e Event facade with the following additional
  303. properties:
  304. <dl>
  305. <dt>oldHash</dt>
  306. <dd>
  307. Previous hash fragment value before the change.
  308. </dd>
  309. <dt>oldUrl</dt>
  310. <dd>
  311. Previous URL (including the hash fragment) before the change.
  312. </dd>
  313. <dt>newHash</dt>
  314. <dd>
  315. New hash fragment value after the change.
  316. </dd>
  317. <dt>newUrl</dt>
  318. <dd>
  319. New URL (including the hash fragment) after the change.
  320. </dd>
  321. </dl>
  322. @for YUI
  323. @since 3.2.0
  324. **/
  325. hashNotifiers = GlobalEnv._notifiers;
  326. if (!hashNotifiers) {
  327. hashNotifiers = GlobalEnv._notifiers = [];
  328. }
  329. Y.Event.define('hashchange', {
  330. on: function (node, subscriber, notifier) {
  331. // Ignore this subscription if the node is anything other than the
  332. // window or document body, since those are the only elements that
  333. // should support the hashchange event. Note that the body could also be
  334. // a frameset, but that's okay since framesets support hashchange too.
  335. if (node.compareTo(win) || node.compareTo(Y.config.doc.body)) {
  336. hashNotifiers.push(notifier);
  337. }
  338. },
  339. detach: function (node, subscriber, notifier) {
  340. var index = YArray.indexOf(hashNotifiers, notifier);
  341. if (index !== -1) {
  342. hashNotifiers.splice(index, 1);
  343. }
  344. }
  345. });
  346. oldHash = HistoryHash.getHash();
  347. oldUrl = HistoryHash.getUrl();
  348. if (HistoryBase.nativeHashChange) {
  349. // Wrap the browser's native hashchange event if there's not already a
  350. // global listener.
  351. if (!GlobalEnv._hashHandle) {
  352. GlobalEnv._hashHandle = Y.Event.attach('hashchange', function (e) {
  353. var newHash = HistoryHash.getHash(),
  354. newUrl = HistoryHash.getUrl();
  355. // Iterate over a copy of the hashNotifiers array since a subscriber
  356. // could detach during iteration and cause the array to be re-indexed.
  357. YArray.each(hashNotifiers.concat(), function (notifier) {
  358. notifier.fire({
  359. _event : e,
  360. oldHash: oldHash,
  361. oldUrl : oldUrl,
  362. newHash: newHash,
  363. newUrl : newUrl
  364. });
  365. });
  366. oldHash = newHash;
  367. oldUrl = newUrl;
  368. }, win);
  369. }
  370. } else {
  371. // Begin polling for location hash changes if there's not already a global
  372. // poll running.
  373. if (!GlobalEnv._hashPoll) {
  374. GlobalEnv._hashPoll = Y.later(50, null, function () {
  375. var newHash = HistoryHash.getHash(),
  376. facade, newUrl;
  377. if (oldHash !== newHash) {
  378. newUrl = HistoryHash.getUrl();
  379. facade = {
  380. oldHash: oldHash,
  381. oldUrl : oldUrl,
  382. newHash: newHash,
  383. newUrl : newUrl
  384. };
  385. oldHash = newHash;
  386. oldUrl = newUrl;
  387. YArray.each(hashNotifiers.concat(), function (notifier) {
  388. notifier.fire(facade);
  389. });
  390. }
  391. }, null, true);
  392. }
  393. }
  394. Y.HistoryHash = HistoryHash;
  395. // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
  396. if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
  397. (!HistoryBase.html5 || !Y.HistoryHTML5))) {
  398. Y.History = HistoryHash;
  399. }