2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('history-hash', function (Y, NAME) {
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 useHistoryHTML5 = Y.config.useHistoryHTML5;
40 function HistoryHash() {
41 HistoryHash.superclass.constructor.apply(this, arguments);
44 Y.extend(HistoryHash, HistoryBase, {
45 // -- Initialization -------------------------------------------------------
46 _init: function (config) {
47 var bookmarkedState = HistoryHash.parseHash();
49 // If an initialState was provided, merge the bookmarked state into it
50 // (the bookmarked state wins).
51 config = config || {};
53 this._initialState = config.initialState ?
54 Y.merge(config.initialState, bookmarkedState) : bookmarkedState;
56 // Subscribe to the synthetic hashchange event (defined below) to handle
58 Y.after('hashchange', Y.bind(this._afterHashChange, this), win);
60 HistoryHash.superclass._init.apply(this, arguments);
63 // -- Protected Methods ----------------------------------------------------
64 _change: function (src, state, options) {
65 // Stringify all values to ensure that comparisons don't fail after
66 // they're coerced to strings in the location hash.
67 YObject.each(state, function (value, key) {
68 if (Lang.isValue(value)) {
69 state[key] = value.toString();
73 return HistoryHash.superclass._change.call(this, src, state, options);
76 _storeState: function (src, newState) {
77 var decode = HistoryHash.decode,
78 newHash = HistoryHash.createHash(newState);
80 HistoryHash.superclass._storeState.apply(this, arguments);
82 // Update the location hash with the changes, but only if the new hash
83 // actually differs from the current hash (this avoids creating multiple
84 // history entries for a single state).
86 // We always compare decoded hashes, since it's possible that the hash
87 // could be set incorrectly to a non-encoded value outside of
89 if (src !== SRC_HASH && decode(HistoryHash.getHash()) !== decode(newHash)) {
90 HistoryHash[src === HistoryBase.SRC_REPLACE ? 'replaceHash' : 'setHash'](newHash);
94 // -- Protected Event Handlers ---------------------------------------------
97 * Handler for hashchange events.
99 * @method _afterHashChange
103 _afterHashChange: function (e) {
104 this._resolveChanges(SRC_HASH, HistoryHash.parseHash(e.newHash), {});
107 // -- Public Static Properties ---------------------------------------------
111 * Constant used to identify state changes originating from
112 * <code>hashchange</code> events.
123 * Prefix to prepend when setting the hash fragment. For example, if the
124 * prefix is <code>!</code> and the hash fragment is set to
125 * <code>#foo=bar&baz=quux</code>, the final hash fragment in the URL will
126 * become <code>#!foo=bar&baz=quux</code>. This can be used to help make an
127 * Ajax application crawlable in accordance with Google's guidelines at
128 * <a href="http://code.google.com/web/ajaxcrawling/">http://code.google.com/web/ajaxcrawling/</a>.
132 * Note that this prefix applies to all HistoryHash instances. It's not
133 * possible for individual instances to use their own prefixes since they
134 * all operate on the same URL.
137 * @property hashPrefix
144 // -- Protected Static Properties ------------------------------------------
147 * Regular expression used to parse location hash/query strings.
149 * @property _REGEX_HASH
155 _REGEX_HASH: /([^\?#&=]+)=?([^&=]*)/g,
157 // -- Public Static Methods ------------------------------------------------
160 * Creates a location hash string from the specified object of key/value
164 * @param {Object} params object of key/value parameter pairs
165 * @return {String} location hash string
168 createHash: function (params) {
169 var encode = HistoryHash.encode,
172 YObject.each(params, function (value, key) {
173 if (Lang.isValue(value)) {
174 hash.push(encode(key) + '=' + encode(value));
178 return hash.join('&');
182 * Wrapper around <code>decodeURIComponent()</code> that also converts +
186 * @param {String} string string to decode
187 * @return {String} decoded string
190 decode: function (string) {
191 return decodeURIComponent(string.replace(/\+/g, ' '));
195 * Wrapper around <code>encodeURIComponent()</code> that converts spaces to
199 * @param {String} string string to encode
200 * @return {String} encoded string
203 encode: function (string) {
204 return encodeURIComponent(string).replace(/%20/g, '+');
208 * Gets the raw (not decoded) current location hash, minus the preceding '#'
209 * character and the hashPrefix (if one is set).
212 * @return {String} current location hash
215 getHash: (Y.UA.gecko ? function () {
216 // Gecko's window.location.hash returns a decoded string and we want all
217 // encoding untouched, so we need to get the hash value from
218 // window.location.href instead. We have to use UA sniffing rather than
219 // feature detection, since the only way to detect this would be to
220 // actually change the hash.
221 var location = Y.getLocation(),
222 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 location = Y.getLocation(),
230 hash = location.hash.substring(1),
231 prefix = HistoryHash.hashPrefix;
233 // Slight code duplication here, but execution speed is of the essence
234 // since getHash() is called every 50ms to poll for changes in browsers
235 // that don't support native onhashchange. An additional function call
236 // would add unnecessary overhead.
237 return prefix && hash.indexOf(prefix) === 0 ?
238 hash.replace(prefix, '') : hash;
242 * Gets the current bookmarkable URL.
245 * @return {String} current bookmarkable URL
248 getUrl: function () {
249 return location.href;
253 * Parses a location hash string into an object of key/value parameter
254 * pairs. If <i>hash</i> is not specified, the current location hash will
258 * @param {String} hash (optional) location hash string
259 * @return {Object} object of parsed key/value parameter pairs
262 parseHash: function (hash) {
263 var decode = HistoryHash.decode,
270 prefix = HistoryHash.hashPrefix,
273 hash = Lang.isValue(hash) ? hash : HistoryHash.getHash();
276 prefixIndex = hash.indexOf(prefix);
278 if (prefixIndex === 0 || (prefixIndex === 1 && hash.charAt(0) === '#')) {
279 hash = hash.replace(prefix, '');
283 matches = hash.match(HistoryHash._REGEX_HASH) || [];
285 for (i = 0, len = matches.length; i < len; ++i) {
288 param = match.split('=');
290 if (param.length > 1) {
291 params[decode(param[0])] = decode(param[1]);
293 params[decode(match)] = '';
301 * Replaces the browser's current location hash with the specified hash
302 * and removes all forward navigation states, without creating a new browser
303 * history entry. Automatically prepends the <code>hashPrefix</code> if one
306 * @method replaceHash
307 * @param {String} hash new location hash
310 replaceHash: function (hash) {
311 var location = Y.getLocation(),
312 base = location.href.replace(/#.*$/, '');
314 if (hash.charAt(0) === '#') {
315 hash = hash.substring(1);
318 location.replace(base + '#' + (HistoryHash.hashPrefix || '') + hash);
322 * Sets the browser's location hash to the specified string. Automatically
323 * prepends the <code>hashPrefix</code> if one is set.
326 * @param {String} hash new location hash
329 setHash: function (hash) {
330 var location = Y.getLocation();
332 if (hash.charAt(0) === '#') {
333 hash = hash.substring(1);
336 location.hash = (HistoryHash.hashPrefix || '') + hash;
340 // -- Synthetic hashchange Event -----------------------------------------------
342 // TODO: YUIDoc currently doesn't provide a good way to document synthetic DOM
343 // events. For now, we're just documenting the hashchange event on the YUI
344 // object, which is about the best we can do until enhancements are made to
348 Synthetic <code>window.onhashchange</code> event that normalizes differences
349 across browsers and provides support for browsers that don't natively support
350 <code>onhashchange</code>.
352 This event is provided by the <code>history-hash</code> module.
356 YUI().use('history-hash', function (Y) {
357 Y.on('hashchange', function (e) {
358 // Handle hashchange events on the current window.
363 @param {EventFacade} e Event facade with the following additional
369 Previous hash fragment value before the change.
374 Previous URL (including the hash fragment) before the change.
379 New hash fragment value after the change.
384 New URL (including the hash fragment) after the change.
391 hashNotifiers = GlobalEnv._notifiers;
393 if (!hashNotifiers) {
394 hashNotifiers = GlobalEnv._notifiers = [];
397 Y.Event.define('hashchange', {
398 on: function (node, subscriber, notifier) {
399 // Ignore this subscription if the node is anything other than the
400 // window or document body, since those are the only elements that
401 // should support the hashchange event. Note that the body could also be
402 // a frameset, but that's okay since framesets support hashchange too.
403 if (node.compareTo(win) || node.compareTo(Y.config.doc.body)) {
404 hashNotifiers.push(notifier);
408 detach: function (node, subscriber, notifier) {
409 var index = YArray.indexOf(hashNotifiers, notifier);
412 hashNotifiers.splice(index, 1);
417 oldHash = HistoryHash.getHash();
418 oldUrl = HistoryHash.getUrl();
420 if (HistoryBase.nativeHashChange) {
421 // Wrap the browser's native hashchange event if there's not already a
423 if (!GlobalEnv._hashHandle) {
424 GlobalEnv._hashHandle = Y.Event.attach('hashchange', function (e) {
425 var newHash = HistoryHash.getHash(),
426 newUrl = HistoryHash.getUrl();
428 // Iterate over a copy of the hashNotifiers array since a subscriber
429 // could detach during iteration and cause the array to be re-indexed.
430 YArray.each(hashNotifiers.concat(), function (notifier) {
445 // Begin polling for location hash changes if there's not already a global
447 if (!GlobalEnv._hashPoll) {
448 GlobalEnv._hashPoll = Y.later(50, null, function () {
449 var newHash = HistoryHash.getHash(),
452 if (oldHash !== newHash) {
453 newUrl = HistoryHash.getUrl();
465 YArray.each(hashNotifiers.concat(), function (notifier) {
466 notifier.fire(facade);
473 Y.HistoryHash = HistoryHash;
475 // HistoryHash will never win over HistoryHTML5 unless useHistoryHTML5 is false.
476 if (useHistoryHTML5 === false || (!Y.History && useHistoryHTML5 !== true &&
477 (!HistoryBase.html5 || !Y.HistoryHTML5))) {
478 Y.History = HistoryHash;
482 }, '3.13.0', {"requires": ["event-synthetic", "history-base", "yui-later"]});