Skip to content

Instantly share code, notes, and snippets.

@nfeldman
Created August 9, 2011 01:14
Show Gist options
  • Save nfeldman/1133214 to your computer and use it in GitHub Desktop.
Save nfeldman/1133214 to your computer and use it in GitHub Desktop.
PathActions
/**
* @fileoveriew A very rough sketch for location hooks (or location based
* events) system to simplify and structure tying multiple functions to a path.
* Can be triggered conditionally by binding init() to an event handler or
* on page load. This is version 0.1, so it doesn't do much, but it does
* handle wildcards. Examples of use are provided at the end of this gist
* TODO convert console copy/paste tests to unit tests
* TODO test in IE
* TODO create repo for this project
* @version 0.1
* @author Noah Feldman
* @copyright 2011
*/
// JS Module to simplify client side location relative script execution
var MYAPP = {}; // where MYAPP is your namespace
MYAPP.pathActions = (function (G, U) {
var undef = typeof U,
rLPathTrim = /^(\/)/,
rRPathTrim = /([/*])$/,
rWild = /\*$/,
hasOwn = Object.prototype.hasOwnProperty,
toString = Object.prototype.toString,
slice = Array.prototype.slice,
pathname, arrayOrObject, keys, each;
// ensure consistently formatted strings for comparisons
function prepPath (path) {
path = path.length > 1 ? path.replace(rLPathTrim, '').replace(rRPathTrim, '') : '/';
return path;
}
if (!G.location && !location) // makes mocking a location object even simpler
throw new Error('PathActions requires access to a location object.');
pathname = prepPath(G.location || location);
// helper for deep copy of an object
arrayOrObject = (function () {
var arOrObObj = { '[object Array]': 1, '[object Object]': 2 };
return function (obj) {
return arOrObObj[toString.call(obj)] || 0;
}
}());
// merge function extracted from Fortinbras
function merge (into, from, shallow) {
for (var key in from) {
if (hasOwn.call(from, key)) {
if (!shallow && arrayOrObject(from[key])) {
if (arrayOrObject(from[key]) == 1)
into[key] = slice.call(from[key]);
else
into[key] = merge({}, from[key]);
} else {
into[key] = from[key];
}
}
}
return into;
}
// get an array of an object's own keys
keys = Object.keys || function (obj) {
var ret = [];
for (var key in obj)
if (hasOwn.call(obj, key))
ret.push(key);
return ret;
};
// quick foreach, not fully compatible with native forEach but
// good enough for our needs.
each = (function () {
if (!!Array.prototype.forEach)
return function(array, callback, thisObject) {
return array.forEach(callback, thisObject);
}
else
return function(array, callback, thisObject) {
for (var i = 0, len = array.length; i < len; i++)
callback.call(thisObject, array[i], i, array);
}
}());
function eachKey (obj, callback, thisObject) {
return each(keys(obj), callback, thisObject);
}
// export utilities as methods of MYAPP
MYAPP.eachKey = eachKey;
MYAPP.merge = merge;
// return our "Module", this object is what you access at
// MYAPP.pathActions
return {
segments: pathname.length > 1 ? pathname.split('/') : [],
pathname: pathname,
namedPaths: [],
paths: {},
/**
* @param {string} name, a label that ties an action to a location
* @param {string} path, a location
* @param {object} rules, optionally provide callback at the same time
*/
// we could have made the paths the keys and stored the actions as
// values. That might be a smarter way to go, but this has greater
// flexibility. Whether we use it or not is a different question.
addPath: function (name, path, rules) {
var segs;
if (typeof this.paths[name] == undef) {
this.paths[name] = {};
this.namedPaths.push(name);
this.paths[name].w = rWild.test(path);
path = prepPath(path);
this.paths[name].path = path;
segs = path.length == 1 ? [] : path.split('/');
this.paths[name].l = segs.length;
this.paths[name].segs = segs;
if (rules) {
if (!rules.name)
rules.name = name;
this.addRule(rules);
}
return this;
}
},
/**
* @param {object} rules, currently `name` and `callback`, more to come?
*/
addRule: function (rules) {
var name;
if (!rules.name || !this.paths[rules.name])
throw new Error('Path ' + (rules.name || 'unnamed path') + ' not found');
name = rules.name;
if (this.paths[name].w) {
// a placeholder in case we add black- and/or white-listing with wildcards
this.paths[name].exceptions = {};
merge(this.paths[name].exceptions, rules.exception);
}
this._actions[name] = rules.callback;
return this;
},
init: function () {
var toTest = {},
paths = this.paths,
segs = this.segments,
path = this.pathname,
names = [];
// first we'll collect potential matches
each(this.namedPaths, function(name, i) {
if (paths[name].w) {
if (segs.length >= paths[name].l)
toTest[name] = paths[name];
} else if (segs.length == paths[name].l) {
toTest[name] = paths[name];
}
});
// if I add variable segments, this won't be sufficient, which is
// the other reason the segment arrays already exist, but for now
// this is good.
eachKey(toTest, function(name) {
var rTest = RegExp('(?:' + paths[name].path + ')');
if (paths[name].w && rTest.test(path))
names.push(name);
else if (toTest[name] === path)
names.push(name);
});
if (names) {
for (var i = 0, len = names.length; i < len; i++)
this._actions[names[i]]();
return true;
}
return; // no registered actions for this location
},
_actions: {}
}
}(window || this));
/**
* @example add paths either like this:
MYAPP.pathActions.addPath('test1', '/research/current-research-projects/')
.addRule({
name: 'test1',
callback: function () {
var el = document.getElementById('main');
el.style.opacity = '.5';
console.log('test1 matched!');
}
});
*
* or this:
MYAPP.pathActions.addPath('test2', 'research/*', {
callback: function () {
console.log('wildcard test passed')
}
});
* or this:
MYAPP.pathActions.addPath('test3,' '/products/electronics/brands/*');
// ... do stuff
MYAPP.pathActions.addRule({name: 'test3', callback: someFunction});
* and cram as many named paths as you want in there, then call
* MYAPP.pathActions.init() onload (or click, or whatever)
* This puts all your location based logic in a single object.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment