3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('history-hash', function(Y) {
10 * Provides browser history management backed by
11 * <code>window.location.hash</code>, as well as convenience methods for working
12 * with the location hash and a synthetic <code>hashchange</code> event that
13 * normalizes differences across browsers.
16 * @submodule history-hash
19 * @extends HistoryBase
21 * @param {Object} config (optional) Configuration object. See the HistoryBase
22 * documentation for details.
25 var HistoryBase = Y.HistoryBase,
29 GlobalEnv = YUI.namespace('Env.HistoryHash'),
37 useHistoryHTML5 = Y.config.useHistoryHTML5;
39 function HistoryHash() {
40 HistoryHash.superclass.constructor.apply(this, arguments);
43 Y.extend(HistoryHash, HistoryBase, {
44 // -- Initialization -------------------------------------------------------
45 _init: function (config) {
46 var bookmarkedState = HistoryHash.parseHash();
48 // If an initialState was provided, merge the bookmarked state into it
49 // (the bookmarked state wins).
50 config = config || {};
52 this._initialState = config.initialState ?
53 Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
55 // Subscribe to the synthetic hashchange event (defined below) to handle
57 Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
59 HistoryHash.superclass._init.apply(this, arguments);
62 // -- Protected Methods ----------------------------------------------------
63 _change: function (src, state, options) {
64 // Stringify all values to ensure that comparisons don't fail after
65 // they're coerced to strings in the location hash.
66 YObject.each(state, function (value, key) {
67 if (Lang.isValue(value)) {
68 state[key] = value.toString();
72 return HistoryHash.superclass._change.call(this, src, state, options);
75 _storeState: function (src, newState) {
76 var decode = HistoryHash.decode,
77 newHash = HistoryHash.createHash(newState);
79 HistoryHash.superclass._storeState.apply(this, arguments);
81 // Update the location hash with the changes, but only if the new hash
82 // actually differs from the current hash (this avoids creating multiple
83 // history entries for a single state).
85 // We always compare decoded hashes, since it's possible that the hash
86 // could be set incorrectly to a non-encoded value outside of
88 if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(newHash)) {
89 HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
93 // -- Protected Event Handlers ---------------------------------------------
96 * Handler for hashchange events.
98 * @method _afterHashChange
102 _afterHashChange: function (e) {
103 this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
106 // -- Public Static Properties ---------------------------------------------
110 * Constant used to identify state changes originating from
111 * <code>hashchange</code> events.
122 * Prefix to prepend when setting the hash fragment. For example, if the
123 * prefix is <code>!</code> and the hash fragment is set to
124 * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
125 * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
126 * Ajax application crawlable in accordance with Google's guidelines at
127 * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
131 * Note that this prefix applies to all HistoryHash instances. It's not
132 * possible for individual instances to use their own prefixes since they
133 * all operate on the same URL.
136 * @property hashPrefix
143 // -- Protected Static Properties ------------------------------------------
146 * Regular expression used to parse location hash/query strings.
148 * @property _REGEX_HASH
154 _REGEX_HASH: /([^\?#&]+)=([^&]+)/g,
156 // -- Public Static Methods ------------------------------------------------
159 * Creates a location hash string from the specified object of key/value
163 * @param {Object} params object of key/value parameter pairs
164 * @return {String} location hash string
167 createHash: function (params) {
168 var encode = HistoryHash.encode,
171 YObject.each(params, function (value, key) {
172 if (Lang.isValue(value)) {
173 hash.push(encode(key) + '=' + encode(value));
177 return hash.join('&');
181 * Wrapper around <code>decodeURIComponent()</code> that also converts +
185 * @param {String} string string to decode
186 * @return {String} decoded string
189 decode: function (string) {
190 return decodeURIComponent(string.replace(/\+/g, ' '));
194 * Wrapper around <code>encodeURIComponent()</code> that converts spaces to
198 * @param {String} string string to encode
199 * @return {String} encoded string
202 encode: function (string) {
203 return encodeURIComponent(string).replace(/%20/g, '+');
207 * Gets the raw (not decoded) current location hash, minus the preceding '#'
208 * character and the hashPrefix (if one is set).
211 * @return {String} current location hash
214 getHash: (Y.UA.gecko ? function () {
215 // Gecko's window.location.hash returns a decoded string and we want all
216 // encoding untouched, so we need to get the hash value from
217 // window.location.href instead. We have to use UA sniffing rather than
218 // feature detection, since the only way to detect this would be to
219 // actually change the hash.
220 var location = Y.getLocation(),
221 matches = /#(.*)$/.exec(location.href),
222 hash = matches && matches[1] || '',
223 prefix = HistoryHash.hashPrefix;
225 return prefix && hash.indexOf(prefix) === 0 ?
226 hash.replace(prefix, '') : hash;
228 var location = Y.getLocation(),
229 hash = location.hash.substring(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 var location = Y.getLocation(),
303 base = location.href.replace(/#.*$/, '');
305 if (hash.charAt(0) === '#') {
306 hash = hash.substring(1);
309 location.replace(base + '#' + (HistoryHash.hashPrefix || '') + hash);
313 * Sets the browser's location hash to the specified string. Automatically
314 * prepends the <code>hashPrefix</code> if one is set.
317 * @param {String} hash new location hash
320 setHash: function (hash) {
321 var location = Y.getLocation();
323 if (hash.charAt(0) === '#') {
324 hash = hash.substring(1);
327 location.hash = (HistoryHash.hashPrefix || '') + hash;
331 // -- Synthetic hashchange Event -----------------------------------------------
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
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>.
343 This event is provided by the <code>history-hash</code> module.
347 YUI().use('history-hash', function (Y) {
348 Y.on('hashchange', function (e) {
349 // Handle hashchange events on the current window.
354 @param {EventFacade} e Event facade with the following additional
360 Previous hash fragment value before the change.
365 Previous URL (including the hash fragment) before the change.
370 New hash fragment value after the change.
375 New URL (including the hash fragment) after the change.
382 hashNotifiers = GlobalEnv._notifiers;
384 if (!hashNotifiers) {
385 hashNotifiers = GlobalEnv._notifiers = [];
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);
399 detach: function (node, subscriber, notifier) {
400 var index = YArray.indexOf(hashNotifiers, notifier);
403 hashNotifiers.splice(index, 1);
408 oldHash = HistoryHash.getHash();
409 oldUrl = HistoryHash.getUrl();
411 if (HistoryBase.nativeHashChange) {
412 // Wrap the browser's native hashchange event.
413 Y.Event.attach('hashchange', function (e) {
414 var newHash = HistoryHash.getHash(),
415 newUrl = HistoryHash.getUrl();
417 // Iterate over a copy of the hashNotifiers array since a subscriber
418 // could detach during iteration and cause the array to be re-indexed.
419 YArray.each(hashNotifiers.concat(), function (notifier) {
433 // Begin polling for location hash changes if there's not already a global
435 if (!GlobalEnv._hashPoll) {
436 GlobalEnv._hashPoll = Y.later(50, null, function () {
437 var newHash = HistoryHash.getHash(),
440 if (oldHash !== newHash) {
441 newUrl = HistoryHash.getUrl();
453 YArray.each(hashNotifiers.concat(), function (notifier) {
454 notifier.fire(facade);
461 Y.HistoryHash = HistoryHash;
463 // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
464 if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
465 (!HistoryBase.html5 || !Y.HistoryHTML5))) {
466 Y.History = HistoryHash;
470 }, '3.5.0' ,{requires:['event-synthetic', 'history-base', 'yui-later']});