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