Skip to content

Instantly share code, notes, and snippets.

@lsmith
Forked from rgrove/history-lite.js
Created September 23, 2009 03:37
Show Gist options
  • Save lsmith/191685 to your computer and use it in GitHub Desktop.
Save lsmith/191685 to your computer and use it in GitHub Desktop.
/**
* 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