- /**
- * Provides browser history management backed by
- * <code>window.location.hash</code>, as well as convenience methods for working
- * with the location hash and a synthetic <code>hashchange</code> 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,
- YObject = Y.Object,
- GlobalEnv = YUI.namespace('Env.HistoryHash'),
-
- SRC_HASH = 'hash',
-
- hashNotifiers,
- oldHash,
- oldUrl,
- win = Y.config.win,
- 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 ----------------------------------------------------
- _change: function (src, state, options) {
- // Stringify all values to ensure that comparisons don't fail after
- // they're coerced to strings in the location hash.
- YObject.each(state, function (value, key) {
- if (Lang.isValue(value)) {
- state[key] = value.toString();
- }
- });
-
- return HistoryHash.superclass._change.call(this, src, state, options);
- },
-
- _storeState: function (src, newState) {
- var decode = HistoryHash.decode,
- 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).
- //
- // We always compare decoded hashes, since it's possible that the hash
- // could be set incorrectly to a non-encoded value outside of
- // HistoryHash.
- if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(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
- * <code>hashchange</code> events.
- *
- * @property SRC_HASH
- * @type String
- * @static
- * @final
- */
- SRC_HASH: SRC_HASH,
-
- /**
- * <p>
- * Prefix to prepend when setting the hash fragment. For example, if the
- * prefix is <code>!</code> and the hash fragment is set to
- * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
- * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
- * Ajax application crawlable in accordance with Google's guidelines at
- * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
- * </p>
- *
- * <p>
- * 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.
- * </p>
- *
- * @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 = [];
-
- YObject.each(params, function (value, key) {
- if (Lang.isValue(value)) {
- hash.push(encode(key) + '=' + encode(value));
- }
- });
-
- return hash.join('&');
- },
-
- /**
- * Wrapper around <code>decodeURIComponent()</code> 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 <code>encodeURIComponent()</code> 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 location = Y.getLocation(),
- matches = /#(.*)$/.exec(location.href),
- hash = matches && matches[1] || '',
- prefix = HistoryHash.hashPrefix;
-
- return prefix && hash.indexOf(prefix) === 0 ?
- hash.replace(prefix, '') : hash;
- } : function () {
- var location = Y.getLocation(),
- hash = location.hash.substring(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 <i>hash</i> 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,
- match,
- 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) {
- match = matches[i];
-
- param = match.split('=');
-
- if (param.length > 1) {
- params[decode(param[0])] = decode(param[1]);
- } else {
- params[decode(match)] = '';
- }
- }
-
- 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 <code>hashPrefix</code> if one
- * is set.
- *
- * @method replaceHash
- * @param {String} hash new location hash
- * @static
- */
- replaceHash: function (hash) {
- var location = Y.getLocation(),
- base = location.href.replace(/#.*$/, '');
-
- if (hash.charAt(0) === '#') {
- hash = hash.substring(1);
- }
-
- location.replace(base + '#' + (HistoryHash.hashPrefix || '') + hash);
- },
-
- /**
- * Sets the browser's location hash to the specified string. Automatically
- * prepends the <code>hashPrefix</code> if one is set.
- *
- * @method setHash
- * @param {String} hash new location hash
- * @static
- */
- setHash: function (hash) {
- var location = Y.getLocation();
-
- if (hash.charAt(0) === '#') {
- hash = hash.substring(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 <code>window.onhashchange</code> event that normalizes differences
- across browsers and provides support for browsers that don't natively support
- <code>onhashchange</code>.
-
- This event is provided by the <code>history-hash</code> module.
-
- @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:
-
- <dl>
- <dt>oldHash</dt>
- <dd>
- Previous hash fragment value before the change.
- </dd>
-
- <dt>oldUrl</dt>
- <dd>
- Previous URL (including the hash fragment) before the change.
- </dd>
-
- <dt>newHash</dt>
- <dd>
- New hash fragment value after the change.
- </dd>
-
- <dt>newUrl</dt>
- <dd>
- New URL (including the hash fragment) after the change.
- </dd>
- </dl>
- @for YUI
- @since 3.2.0
- **/
-
- hashNotifiers = GlobalEnv._notifiers;
-
- if (!hashNotifiers) {
- hashNotifiers = GlobalEnv._notifiers = [];
- }
-
- Y.Event.define('hashchange', {
- on: function (node, subscriber, notifier) {
- // Ignore this subscription if the node is anything other than the
- // window or document body, since those are the only elements that
- // should support the hashchange event. Note that the body could also be
- // a frameset, but that's okay since framesets support hashchange too.
- if (node.compareTo(win) || node.compareTo(Y.config.doc.body)) {
- hashNotifiers.push(notifier);
- }
- },
-
- detach: function (node, subscriber, notifier) {
- var index = YArray.indexOf(hashNotifiers, notifier);
-
- if (index !== -1) {
- hashNotifiers.splice(index, 1);
- }
- }
- });
-
- oldHash = HistoryHash.getHash();
- oldUrl = HistoryHash.getUrl();
-
- if (HistoryBase.nativeHashChange) {
- // Wrap the browser's native hashchange event if there's not already a
- // global listener.
- if (!GlobalEnv._hashHandle) {
- GlobalEnv._hashHandle = Y.Event.attach('hashchange', function (e) {
- var newHash = HistoryHash.getHash(),
- newUrl = HistoryHash.getUrl();
-
- // Iterate over a copy of the hashNotifiers array since a subscriber
- // could detach during iteration and cause the array to be re-indexed.
- YArray.each(hashNotifiers.concat(), function (notifier) {
- notifier.fire({
- _event : e,
- oldHash: oldHash,
- oldUrl : oldUrl,
- newHash: newHash,
- newUrl : newUrl
- });
- });
-
- oldHash = newHash;
- oldUrl = newUrl;
- }, win);
- }
- } else {
- // Begin polling for location hash changes if there's not already a global
- // poll running.
- if (!GlobalEnv._hashPoll) {
- GlobalEnv._hashPoll = Y.later(50, null, function () {
- var newHash = HistoryHash.getHash(),
- facade, newUrl;
-
- if (oldHash !== newHash) {
- newUrl = HistoryHash.getUrl();
-
- facade = {
- oldHash: oldHash,
- oldUrl : oldUrl,
- newHash: newHash,
- newUrl : newUrl
- };
-
- oldHash = newHash;
- oldUrl = newUrl;
-
- YArray.each(hashNotifiers.concat(), function (notifier) {
- notifier.fire(facade);
- });
- }
- }, null, true);
- }
- }
-
- Y.HistoryHash = HistoryHash;
-
- // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
- if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
- (!HistoryBase.html5 || !Y.HistoryHTML5))) {
- Y.History = HistoryHash;
- }
-
-