2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('history-hash', function(Y) {
11 * Provides browser history management backed by
12 * <code>window.location.hash</code>, as well as convenience methods for working
13 * with the location hash and a synthetic <code>hashchange</code> event that
14 * normalizes differences across browsers.
17 * @submodule history-hash
20 * @extends HistoryBase
22 * @param {Object} config (optional) Configuration object. See the HistoryBase
23 * documentation for details.
26 var HistoryBase = Y.HistoryBase,
30 GlobalEnv = YUI.namespace('Env.HistoryHash'),
38 location = win.location,
39 useHistoryHTML5 = Y.config.useHistoryHTML5;
41 function HistoryHash() {
42 HistoryHash.superclass.constructor.apply(this, arguments);
45 Y.extend(HistoryHash, HistoryBase, {
46 // -- Initialization -------------------------------------------------------
47 _init: function (config) {
48 var bookmarkedState = HistoryHash.parseHash();
50 // If an initialState was provided, merge the bookmarked state into it
51 // (the bookmarked state wins).
52 config = config || {};
54 this._initialState = config.initialState ?
55 Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
57 // Subscribe to the synthetic hashchange event (defined below) to handle
59 Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
61 HistoryHash.superclass._init.apply(this, arguments);
64 // -- Protected Methods ----------------------------------------------------
65 _change: function (src, state, options) {
66 // Stringify all values to ensure that comparisons don't fail after
67 // they're coerced to strings in the location hash.
68 YObject.each(state, function (value, key) {
69 if (Lang.isValue(value)) {
70 state[key] = value.toString();
74 return HistoryHash.superclass._change.call(this, src, state, options);
77 _storeState: function (src, newState) {
78 var decode = HistoryHash.decode,
79 newHash = HistoryHash.createHash(newState);
81 HistoryHash.superclass._storeState.apply(this, arguments);
83 // Update the location hash with the changes, but only if the new hash
84 // actually differs from the current hash (this avoids creating multiple
85 // history entries for a single state).
87 // We always compare decoded hashes, since it's possible that the hash
88 // could be set incorrectly to a non-encoded value outside of
90 if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(newHash)) {
91 HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
95 // -- Protected Event Handlers ---------------------------------------------
98 * Handler for hashchange events.
100 * @method _afterHashChange
104 _afterHashChange: function (e) {
105 this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
108 // -- Public Static Properties ---------------------------------------------
112 * Constant used to identify state changes originating from
113 * <code>hashchange</code> events.
124 * Prefix to prepend when setting the hash fragment. For example, if the
125 * prefix is <code>!</code> and the hash fragment is set to
126 * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
127 * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
128 * Ajax application crawlable in accordance with Google's guidelines at
129 * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
133 * Note that this prefix applies to all HistoryHash instances. It's not
134 * possible for individual instances to use their own prefixes since they
135 * all operate on the same URL.
138 * @property hashPrefix
145 // -- Protected Static Properties ------------------------------------------
148 * Regular expression used to parse location hash/query strings.
150 * @property _REGEX_HASH
156 _REGEX_HASH: /([^\?#&]+)=([^&]+)/g,
158 // -- Public Static Methods ------------------------------------------------
161 * Creates a location hash string from the specified object of key/value
165 * @param {Object} params object of key/value parameter pairs
166 * @return {String} location hash string
169 createHash: function (params) {
170 var encode = HistoryHash.encode,
173 YObject.each(params, function (value, key) {
174 if (Lang.isValue(value)) {
175 hash.push(encode(key) + '=' + encode(value));
179 return hash.join('&');
183 * Wrapper around <code>decodeURIComponent()</code> that also converts +
187 * @param {String} string string to decode
188 * @return {String} decoded string
191 decode: function (string) {
192 return decodeURIComponent(string.replace(/\+/g, ' '));
196 * Wrapper around <code>encodeURIComponent()</code> that converts spaces to
200 * @param {String} string string to encode
201 * @return {String} encoded string
204 encode: function (string) {
205 return encodeURIComponent(string).replace(/%20/g, '+');
209 * Gets the raw (not decoded) current location hash, minus the preceding '#'
210 * character and the hashPrefix (if one is set).
213 * @return {String} current location hash
216 getHash: (Y.UA.gecko ? function () {
217 // Gecko's window.location.hash returns a decoded string and we want all
218 // encoding untouched, so we need to get the hash value from
219 // window.location.href instead. We have to use UA sniffing rather than
220 // feature detection, since the only way to detect this would be to
221 // actually change the hash.
222 var matches = /#(.*)$/.exec(location.href),
223 hash = matches && matches[1] || '',
224 prefix = HistoryHash.hashPrefix;
226 return prefix && hash.indexOf(prefix) === 0 ?
227 hash.replace(prefix, '') : hash;
229 var hash = location.hash.substr(1),
230 prefix = HistoryHash.hashPrefix;
232 // Slight code duplication here, but execution speed is of the essence
233 // since getHash() is called every 50ms to poll for changes in browsers
234 // that don't support native onhashchange. An additional function call
235 // would add unnecessary overhead.
236 return prefix && hash.indexOf(prefix) === 0 ?
237 hash.replace(prefix, '') : hash;
241 * Gets the current bookmarkable URL.
244 * @return {String} current bookmarkable URL
247 getUrl: function () {
248 return location.href;
252 * Parses a location hash string into an object of key/value parameter
253 * pairs. If <i>hash</i> is not specified, the current location hash will
257 * @param {String} hash (optional) location hash string
258 * @return {Object} object of parsed key/value parameter pairs
261 parseHash: function (hash) {
262 var decode = HistoryHash.decode,
268 prefix = HistoryHash.hashPrefix,
271 hash = Lang.isValue(hash) ? hash : HistoryHash.getHash();
274 prefixIndex = hash.indexOf(prefix);
276 if (prefixIndex === 0 || (prefixIndex === 1 && hash.charAt(0) === '#')) {
277 hash = hash.replace(prefix, '');
281 matches = hash.match(HistoryHash._REGEX_HASH) || [];
283 for (i = 0, len = matches.length; i < len; ++i) {
284 param = matches[i].split('=');
285 params[decode(param[0])] = decode(param[1]);
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
297 * @method replaceHash
298 * @param {String} hash new location hash
301 replaceHash: function (hash) {
302 if (hash.charAt(0) === '#') {
303 hash = hash.substr(1);
306 location.replace('#' + (HistoryHash.hashPrefix || '') + hash);
310 * Sets the browser's location hash to the specified string. Automatically
311 * prepends the <code>hashPrefix</code> if one is set.
314 * @param {String} hash new location hash
317 setHash: function (hash) {
318 if (hash.charAt(0) === '#') {
319 hash = hash.substr(1);
322 location.hash = (HistoryHash.hashPrefix || '') + hash;
326 // -- Synthetic hashchange Event -----------------------------------------------
328 // TODO: YUIDoc currently doesn't provide a good way to document synthetic DOM
329 // events. For now, we're just documenting the hashchange event on the YUI
330 // object, which is about the best we can do until enhancements are made to
335 * Synthetic <code>window.onhashchange</code> event that normalizes differences
336 * across browsers and provides support for browsers that don't natively support
337 * <code>onhashchange</code>.
341 * This event is provided by the <code>history-hash</code> module.
345 * <strong>Usage example:</strong>
349 * YUI().use('history-hash', function (Y) {
350 * Y.on('hashchange', function (e) {
351 * // Handle hashchange events on the current window.
352 * }, Y.config.win);
357 * @param {EventFacade} e Event facade with the following additional
363 * Previous hash fragment value before the change.
368 * Previous URL (including the hash fragment) before the change.
373 * New hash fragment value after the change.
378 * New URL (including the hash fragment) after the change.
385 hashNotifiers = GlobalEnv._notifiers;
387 if (!hashNotifiers) {
388 hashNotifiers = GlobalEnv._notifiers = [];
391 Y.Event.define('hashchange', {
392 on: function (node, subscriber, notifier) {
393 // Ignore this subscription if the node is anything other than the
394 // window or document body, since those are the only elements that
395 // should support the hashchange event. Note that the body could also be
396 // a frameset, but that's okay since framesets support hashchange too.
397 if (node.compareTo(win) || node.compareTo(Y.config.doc.body)) {
398 hashNotifiers.push(notifier);
402 detach: function (node, subscriber, notifier) {
403 var index = YArray.indexOf(hashNotifiers, notifier);
406 hashNotifiers.splice(index, 1);
411 oldHash = HistoryHash.getHash();
412 oldUrl = HistoryHash.getUrl();
414 if (HistoryBase.nativeHashChange) {
415 // Wrap the browser's native hashchange event.
416 Y.Event.attach('hashchange', function (e) {
417 var newHash = HistoryHash.getHash(),
418 newUrl = HistoryHash.getUrl();
420 // Iterate over a copy of the hashNotifiers array since a subscriber
421 // could detach during iteration and cause the array to be re-indexed.
422 YArray.each(hashNotifiers.concat(), function (notifier) {
436 // Begin polling for location hash changes if there's not already a global
438 if (!GlobalEnv._hashPoll) {
439 if (Y.UA.webkit && !Y.UA.chrome &&
440 navigator.vendor.indexOf('Apple') !== -1) {
441 // Attach a noop unload handler to disable Safari's back/forward
442 // cache. This works around a nasty Safari bug when the back button
443 // is used to return from a page on another domain, but results in
444 // slightly worse performance. This bug is not present in Chrome.
446 // Unfortunately a UA sniff is unavoidable here, but the
447 // consequences of a false positive are minor.
449 // Current as of Safari 5.0 (6533.16).
450 // See: https://bugs.webkit.org/show_bug.cgi?id=34679
451 Y.on('unload', function () {}, win);
454 GlobalEnv._hashPoll = Y.later(50, null, function () {
455 var newHash = HistoryHash.getHash(),
458 if (oldHash !== newHash) {
459 newUrl = HistoryHash.getUrl();
461 YArray.each(hashNotifiers.concat(), function (notifier) {
477 Y.HistoryHash = HistoryHash;
479 // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
480 if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
481 (!HistoryBase.html5 || !Y.HistoryHTML5))) {
482 Y.History = HistoryHash;
486 }, '3.3.0' ,{requires:['event-synthetic', 'history-base', 'yui-later']});