-
-
Save lsmith/191685 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The History Lite utility is similar in purpose to the YUI Browser History | |
* utility, but with a more flexible API, no initialization or markup | |
* requirements, limited IE6/7 support, and a much smaller footprint. | |
* | |
* @module history-lite | |
*/ | |
/** | |
* @class HistoryLite | |
* @static | |
*/ | |
// -- Note it's YUI, not YUI() | |
YUI.add('history-lite', function (Y) { | |
// -- Use Y.config.win and Y.config.doc instead of window and document | |
var w = Y.config.win, | |
// -- You were using w.location.___ a lot | |
loc = w.location, | |
// -- d was never referenced other than d.documentMode | |
docMode = Y.config.doc.documentMode, | |
// -- Personal pref to create a boolean var immediately. Having a test that | |
// -- only needs to be run once live inside a function is less future proof, | |
// -- imo, even if today the function is only called once. | |
// IE8 supports the hashchange event, but only in IE8 Standards | |
// Mode. However, IE8 in IE7 compatibility mode still defines the | |
// event (but never fires it), so we can't just sniff for the event. We | |
// also can't just sniff for IE8, since other browsers will eventually | |
// support this event as well. Thanks Microsoft! | |
supports_hashchange = w.onhashchange !== undefined && | |
(docMode === undefined || docMode > 8), | |
encode = encodeURIComponent, | |
lastHash, | |
pollInterval, | |
// -- self is typically used to store class instances. This is a static | |
// -- class, so more readable to make the code read that way. | |
HistoryLite, | |
/** | |
* Fired when the history state changes. | |
* | |
// -- I'm not sold on the event namespacing for events that don't bubble, | |
// -- but that's a separate discussion. This seems like a reasonable | |
// -- candidate for global bubbling, but that's up to you. | |
* @event history-lite:change | |
// -- IMO all events should broadcast a single object as the first | |
// -- arg to the subscribers. Event details are on the object/facade. | |
* @param {EventFacade} Event facade with the following additional | |
* properties: | |
* <ul> | |
* <li>changedParams name:value pairs of history parameters that have been | |
* added or changed</li> | |
* <li>removedParams name:value pairs of history parameters that have been | |
* removed (values are the old values)</li> | |
* </ul> | |
*/ | |
EV_HISTORY_CHANGE = 'history-lite:change'; | |
// -- Private Methods ------------------------------------------------------ | |
/** | |
* Creates a hash string from the specified object of name/value parameter | |
* pairs. | |
* | |
* @method createHash | |
* @param {Object} params name/value parameter pairs | |
* @return {String} hash string | |
* @private | |
*/ | |
function createHash(params) { | |
var hash = [], | |
name, value; | |
// -- You could do this and other for loops with Y.each if you're willing | |
// -- to accept the function call overhead | |
for (name in params) { | |
if (params.hasOwnProperty(name)) { | |
value = params[name]; | |
if (Y.Lang.isValue(value)) { | |
hash.push(encode(name) + '=' + 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 | |
* @private | |
*/ | |
function decode(string) { | |
return decodeURIComponent(string.replace(/\+/g, ' ')); | |
} | |
/** | |
* Gets the current URL hash. | |
* | |
* @method getHash | |
* @return {String} | |
* @private | |
*/ | |
var getHash; | |
if (Y.UA.gecko) { | |
// We branch at runtime for Gecko since window.location.hash in Gecko | |
// returns a decoded string, and we want all encoding untouched. | |
getHash = function () { | |
var matches = /#.*$/.exec(loc.href); | |
return matches && matches[0] ? matches[0] : ''; | |
}; | |
} else { | |
getHash = function () { | |
return loc.hash; | |
}; | |
} | |
/** | |
* Sets the browser's location hash to the specified string. | |
* | |
* @method setHash | |
* @param {String} hash | |
* @private | |
*/ | |
function setHash(hash) { | |
loc.hash = hash; | |
} | |
/** | |
* Begins polling to check for location hash changes. | |
* | |
* @method startPolling | |
* @private | |
*/ | |
// -- This is called once privately. Why not just include the inline code? | |
function startPolling() { | |
lastHash = getHash(); | |
if (supports_hashchange) { | |
Y.on('hashchange', function () { | |
handleHashChange(getHash()); | |
}, w); | |
} else { | |
// Y.later for consistency, but setInterval is fine, really | |
pollInterval = pollInterval || Y.later(50, Y.HistoryLite, function () { | |
var hash = getHash(); | |
if (hash !== lastHash) { | |
handleHashChange(hash); | |
} | |
}, null, true); | |
} | |
} | |
// -- Private Event Handlers ----------------------------------------------- | |
/** | |
* Handles changes to the location hash and fires the history-lite:change | |
* event if necessary. | |
* | |
* @method handleHashChange | |
* @param {String} newHash new hash value | |
* @private | |
*/ | |
function handleHashChange(newHash) { | |
var lastParsed = HistoryLite.parseQuery(lastHash), | |
newParsed = HistoryLite.parseQuery(newHash), | |
changedParams = {}, | |
removedParams = {}, | |
isChanged, name; | |
// Figure out what changed. | |
for (name in newParsed) { | |
if (newParsed.hasOwnProperty(name) && | |
lastParsed[name] !== newParsed[name]) { | |
changedParams[name] = newParsed[name]; | |
isChanged = true; | |
} | |
} | |
// Figure out what was removed. | |
for (name in lastParsed) { | |
if (lastParsed.hasOwnProperty(name) && | |
!newParsed.hasOwnProperty(name)) { | |
removedParams[name] = lastParsed[name]; | |
isChanged = true; | |
} | |
} | |
if (isChanged) { | |
// -- moved the updating of lastHash to the default function so | |
// -- implementers could choose to ignore certain types of hash changes | |
// -- when comparing previous to new hash. This might be completely | |
// -- bogus. I didn't give it much thought. | |
// -- Adding more info to the event, and fire with an obj to decorate the | |
// -- event facade. | |
HistoryLite.fire(EV_HISTORY_CHANGE, { | |
prevVal: lastHash, | |
newVal: hash, | |
changed: changedParams, | |
removed: removedParams | |
}); | |
} | |
} | |
// -- This is new per the comment above | |
/** | |
* Default handler for history-lite:change event. Stores the new hash for | |
* later comparison and event triggering. | |
* | |
* @method _defChangeFn | |
* @param e {EventFacade} The event carrying info about the change | |
* @private | |
*/ | |
function _defChangeFn(e) { | |
lastHash = e.newVal; | |
} | |
HistoryLite = { | |
// -- Public Methods --------------------------------------------------- | |
/** | |
* Adds a history entry with changes to the specified parameters. Any | |
* parameters with a <code>null</code> or <code>undefined</code> value | |
* will be removed from the new history entry. | |
* | |
* @method add | |
* @param {String|Object} params query string, hash string, or object | |
* containing name/value parameter pairs | |
// -- convention is 'silent' | |
* @param {Boolean} silent if <em>true</em>, a history change event will | |
* not be fired for this change | |
*/ | |
add: function (params, silent) { | |
var newHash = createHash(Y.merge(HistoryLite.parseQuery(getHash()), | |
Y.Lang.isString(params) ? | |
HistoryLite.parseQuery(params) : | |
params)); | |
if (silent) { | |
// -- The updating of lastHash was delegated to a function, so use it | |
_defChangeFn({ newVal: newHash }); | |
} | |
setHash(newHash); | |
}, | |
/** | |
* Gets the current value of the specified history parameter, or an | |
* object of name/value pairs for all current values if no parameter | |
* name is specified. | |
* | |
* @method get | |
* @param {String} name (optional) parameter name | |
* @return {Object|mixed} | |
*/ | |
get: function (name) { | |
var params = HistoryLite.parseQuery(getHash()); | |
return name ? params[name] : params; | |
}, | |
/** | |
* Parses a query string or hash string into an object of name/value | |
* parameter pairs. | |
* | |
* @method parseQuery | |
* @param {String} query query string or hash string | |
* @return {Object} | |
*/ | |
parseQuery: function (query) { | |
var matches = query.match(/([^\?#&]+)=([^&]+)/g) || [], | |
params = {}, | |
i, len, param; | |
for (i = 0, len = matches.length; i < len; ++i) { | |
param = matches[i].split('='); | |
params[decode(param[0])] = decode(param[1]); | |
} | |
return params; | |
} | |
}; | |
// -- IMHO, all custom events should use an event facade. | |
Y.augment(HistoryLite, Y.Event.Target, true, null, { emitFacade: true }); | |
HistoryLite.publish('history-lite:hashchange', { defaultFn: _defChangeFn }); | |
Y.HistoryLite = HistoryLite; | |
// -- per a previous comment, why not just include the function body inline? | |
startPolling(); | |
// -- event-custom should suffice. lang and ua are in yui. You're not using node. | |
// -- use: [ ... ] is for rollup modules. You're looking for requires. You don't | |
// -- need to specify skinnable. | |
}, '3.0.0b1', { requires: ['event-custom' ]}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment