/* YUI 3.17.2 (build 9c3c78e) Copyright 2014 Yahoo! Inc. All rights reserved. Licensed under the BSD License. http://yuilibrary.com/license/ */ YUI.add('history-hash', function (Y, NAME) { /** * 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, 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 * 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 = []; YObject.each(params, function (value, key) { if (Lang.isValue(value)) { hash.push(encode(key) + '=' + encode(value)); } }); return hash.join('&'); }, /** * Wrapper around decodeURIComponent() 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 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 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, 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 hashPrefix 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 hashPrefix 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 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. @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:
oldHash
Previous hash fragment value before the change.
oldUrl
Previous URL (including the hash fragment) before the change.
newHash
New hash fragment value after the change.
newUrl
New URL (including the hash fragment) after the change.
@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; } }, '3.17.2', {"requires": ["event-synthetic", "history-base", "yui-later"]});