/*
Copyright (c) 2010, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 3.2.0
build: 2676
*/
YUI.add('history-hash', function(Y) {
/**
* Provides browser history management backed by
* window.location.hash
, as well as convenience methods for working
* with the location hash and a synthetic hashchange
event that
* normalizes differences across browsers.
*
* @module history
* @submodule history-hash
* @since 3.2.0
* @class HistoryHash
* @extends HistoryBase
* @constructor
* @param {Object} config (optional) Configuration object. See the HistoryBase
* documentation for details.
*/
var HistoryBase = Y.HistoryBase,
Lang = Y.Lang,
YArray = Y.Array,
GlobalEnv = YUI.namespace('Env.HistoryHash'),
SRC_HASH = 'hash',
hashNotifiers,
oldHash,
oldUrl,
win = Y.config.win,
location = win.location,
useHistoryHTML5 = Y.config.useHistoryHTML5;
function HistoryHash() {
HistoryHash.superclass.constructor.apply(this, arguments);
}
Y.extend(HistoryHash, HistoryBase, {
// -- Initialization -------------------------------------------------------
_init: function (config) {
var bookmarkedState = HistoryHash.parseHash();
// If an initialState was provided, merge the bookmarked state into it
// (the bookmarked state wins).
config = config || {};
this._initialState = config.initialState ?
Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
// Subscribe to the synthetic hashchange event (defined below) to handle
// changes.
Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
HistoryHash.superclass._init.apply(this, arguments);
},
// -- Protected Methods ----------------------------------------------------
_storeState: function (src, newState) {
var newHash = HistoryHash.createHash(newState);
HistoryHash.superclass._storeState.apply(this, arguments);
// Update the location hash with the changes, but only if the new hash
// actually differs from the current hash (this avoids creating multiple
// history entries for a single state).
if (HistoryHash.getHash() !== newHash) {
HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
}
},
// -- Protected Event Handlers ---------------------------------------------
/**
* Handler for hashchange events.
*
* @method _afterHashChange
* @param {Event} e
* @protected
*/
_afterHashChange: function (e) {
this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
}
}, {
// -- Public Static Properties ---------------------------------------------
NAME: 'historyHash',
/**
* Constant used to identify state changes originating from
* hashchange
events.
*
* @property SRC_HASH
* @type String
* @static
* @final
*/
SRC_HASH: SRC_HASH,
/**
*
* Prefix to prepend when setting the hash fragment. For example, if the
* prefix is !
and the hash fragment is set to
* #foo=bar&baz=quux
, the final hash fragment in the URL will
* become #!foo=bar&baz=quux
. This can be used to help make an
* Ajax application crawlable in accordance with Google's guidelines at
* http://code.google.com/web/ajaxcrawling/.
*
* Note that this prefix applies to all HistoryHash instances. It's not * possible for individual instances to use their own prefixes since they * all operate on the same URL. *
* * @property hashPrefix * @type String * @default '' * @static */ hashPrefix: '', // -- Protected Static Properties ------------------------------------------ /** * Regular expression used to parse location hash/query strings. * * @property _REGEX_HASH * @type RegExp * @protected * @static * @final */ _REGEX_HASH: /([^\?#&]+)=([^&]+)/g, // -- Public Static Methods ------------------------------------------------ /** * Creates a location hash string from the specified object of key/value * pairs. * * @method createHash * @param {Object} params object of key/value parameter pairs * @return {String} location hash string * @static */ createHash: function (params) { var encode = HistoryHash.encode, hash = []; Y.Object.each(params, function (value, key) { if (Lang.isValue(value)) { hash.push(encode(key) + '=' + encode(value)); } }); return hash.join('&'); }, /** * Wrapper arounddecodeURIComponent()
that also converts +
* chars into spaces.
*
* @method decode
* @param {String} string string to decode
* @return {String} decoded string
* @static
*/
decode: function (string) {
return decodeURIComponent(string.replace(/\+/g, ' '));
},
/**
* Wrapper around encodeURIComponent()
that converts spaces to
* + chars.
*
* @method encode
* @param {String} string string to encode
* @return {String} encoded string
* @static
*/
encode: function (string) {
return encodeURIComponent(string).replace(/%20/g, '+');
},
/**
* Gets the raw (not decoded) current location hash, minus the preceding '#'
* character and the hashPrefix (if one is set).
*
* @method getHash
* @return {String} current location hash
* @static
*/
getHash: (Y.UA.gecko ? function () {
// Gecko's window.location.hash returns a decoded string and we want all
// encoding untouched, so we need to get the hash value from
// window.location.href instead. We have to use UA sniffing rather than
// feature detection, since the only way to detect this would be to
// actually change the hash.
var matches = /#(.*)$/.exec(location.href),
hash = matches && matches[1] || '',
prefix = HistoryHash.hashPrefix;
return prefix && hash.indexOf(prefix) === 0 ?
hash.replace(prefix, '') : hash;
} : function () {
var hash = location.hash.substr(1),
prefix = HistoryHash.hashPrefix;
// Slight code duplication here, but execution speed is of the essence
// since getHash() is called every 50ms to poll for changes in browsers
// that don't support native onhashchange. An additional function call
// would add unnecessary overhead.
return prefix && hash.indexOf(prefix) === 0 ?
hash.replace(prefix, '') : hash;
}),
/**
* Gets the current bookmarkable URL.
*
* @method getUrl
* @return {String} current bookmarkable URL
* @static
*/
getUrl: function () {
return location.href;
},
/**
* Parses a location hash string into an object of key/value parameter
* pairs. If hash is not specified, the current location hash will
* be used.
*
* @method parseHash
* @param {String} hash (optional) location hash string
* @return {Object} object of parsed key/value parameter pairs
* @static
*/
parseHash: function (hash) {
var decode = HistoryHash.decode,
i,
len,
matches,
param,
params = {},
prefix = HistoryHash.hashPrefix,
prefixIndex;
hash = Lang.isValue(hash) ? hash : HistoryHash.getHash();
if (prefix) {
prefixIndex = hash.indexOf(prefix);
if (prefixIndex === 0 || (prefixIndex === 1 && hash.charAt(0) === '#')) {
hash = hash.replace(prefix, '');
}
}
matches = hash.match(HistoryHash._REGEX_HASH) || [];
for (i = 0, len = matches.length; i < len; ++i) {
param = matches[i].split('=');
params[decode(param[0])] = decode(param[1]);
}
return params;
},
/**
* Replaces the browser's current location hash with the specified hash
* and removes all forward navigation states, without creating a new browser
* history entry. Automatically prepends the hashPrefix
if one
* is set.
*
* @method replaceHash
* @param {String} hash new location hash
* @static
*/
replaceHash: function (hash) {
if (hash.charAt(0) === '#') {
hash = hash.substr(1);
}
location.replace('#' + (HistoryHash.hashPrefix || '') + hash);
},
/**
* Sets the browser's location hash to the specified string. Automatically
* prepends the hashPrefix
if one is set.
*
* @method setHash
* @param {String} hash new location hash
* @static
*/
setHash: function (hash) {
if (hash.charAt(0) === '#') {
hash = hash.substr(1);
}
location.hash = (HistoryHash.hashPrefix || '') + hash;
}
});
// -- Synthetic hashchange Event -----------------------------------------------
// TODO: YUIDoc currently doesn't provide a good way to document synthetic DOM
// events. For now, we're just documenting the hashchange event on the YUI
// object, which is about the best we can do until enhancements are made to
// YUIDoc.
/**
*
* Synthetic window.onhashchange
event that normalizes differences
* across browsers and provides support for browsers that don't natively support
* onhashchange
.
*
* This event is provided by the history-hash
module.
*
* Usage example: *
* *
* YUI().use('history-hash', function (Y) {
* Y.on('hashchange', function (e) {
* // Handle hashchange events on the current window.
* }, Y.config.win);
* });
*
*
* @event hashchange
* @param {EventFacade} e Event facade with the following additional
* properties:
*
*