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.1' ,{requires:['event-synthetic', 'history-base', 'yui-later']});