You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
14 KiB
361 lines
14 KiB
/*
|
|
* Grapnel Router
|
|
* @author Greg Sabia Tucker <greg@narrowlabs.com>
|
|
* @copyright 2019 Greg Sabia Tucker
|
|
* @link https://github.com/baseprime/grapnel
|
|
* Released under MIT License. See http://opensource.org/licenses/MIT
|
|
*/
|
|
class MyRouter {
|
|
constructor(opts) {
|
|
var self = this; /* Scope reference */
|
|
this.events = {}; /* Event Listeners */
|
|
this.state = null; /* Router state object */
|
|
this.options = opts || {}; /* Options */
|
|
this.options.env = this.options.env || (!!(Object.keys(root).length === 0 && process && process.browser !== true) ? 'server' : 'client');
|
|
this.options.mode = this.options.mode || (!!(this.options.env !== 'server' && this.options.pushState && root.history && root.history.pushState) ? 'pushState' : 'hashchange');
|
|
if ('function' === typeof root.addEventListener) {
|
|
root.addEventListener('hashchange', function () {
|
|
self.trigger('hashchange');
|
|
});
|
|
root.addEventListener('popstate', function (e) {
|
|
/* Make sure popstate doesn't run on init -- this is a common issue with Safari and old versions of Chrome */
|
|
if (self.state && self.state.previousState === null) {
|
|
return false;
|
|
}
|
|
self.trigger('navigate');
|
|
});
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
/**
|
|
* Create a RegExp Route from a string
|
|
* This is the heart of the router and I've made it as small as possible!
|
|
* @param {String} path - Path of route
|
|
* @param {Array} keys - Array of keys to fill
|
|
* @param {Bool} sensitive - Case sensitive comparison
|
|
* @param {Bool} strict - Strict mode
|
|
*/
|
|
MyRouter.regexRoute = function (path, keys, sensitive, strict) {
|
|
if (path instanceof RegExp) {
|
|
return path;
|
|
}
|
|
if (path instanceof Array) {
|
|
path = '(' + path.join('|') + ')';
|
|
}
|
|
/* Build route RegExp */
|
|
path = path.concat(strict ? '' : '/?')
|
|
.replace(/\/\(/g, '(?:/')
|
|
.replace(/\+/g, '__plus__')
|
|
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function (_, slash, format, key, capture, optional) {
|
|
keys.push({name: key, optional: !!optional});
|
|
slash = slash || '';
|
|
return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '');
|
|
})
|
|
.replace(/([\/.])/g, '\\$1')
|
|
.replace(/__plus__/g, '(.+)')
|
|
.replace(/\*/g, '(.*)');
|
|
return new RegExp('^' + path + '$', sensitive ? '' : 'i');
|
|
};
|
|
/**
|
|
* ForEach workaround utility
|
|
* @param {Array} a - to iterate
|
|
* @param {Function} callback
|
|
*/
|
|
MyRouter._forEach = function (a, callback) {
|
|
if (typeof Array.prototype.forEach === 'function') {
|
|
return Array.prototype.forEach.call(a, callback);
|
|
}
|
|
/* Replicate forEach() */
|
|
return function (c, next) {
|
|
for (var i = 0, n = this.length; i < n; ++i) {
|
|
c.call(next, this[i], i, this);
|
|
}
|
|
}.call(a, callback);
|
|
};
|
|
/**
|
|
* Add an route and handler
|
|
* @param {String|RegExp} route name
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.prototype.get = MyRouter.prototype.add = function (route) {
|
|
var self = this, middleware = Array.prototype.slice.call(arguments, 1, -1),
|
|
handler = Array.prototype.slice.call(arguments, -1)[0], request = new Request(route);
|
|
var invoke = function RouteHandler() {
|
|
/* Build request parameters */
|
|
var req = request.parse(self.path());
|
|
/* Check if matches are found */
|
|
if (req.match) {
|
|
/* Match found */
|
|
var extra = {route: route, params: req.params, req: req, regex: req.match};
|
|
/* Create call stack -- add middleware first, then handler */
|
|
var stack = new CallStack(self, extra).enqueue(middleware.concat(handler));
|
|
/* Trigger main event */
|
|
self.trigger('match', stack, req);
|
|
/* Continue */
|
|
if (!stack.runCallback) {
|
|
return self;
|
|
}
|
|
/* Previous state becomes current state */
|
|
stack.previousState = self.state;
|
|
/* Save new state */
|
|
self.state = stack;
|
|
/* Prevent this handler from being called if parent handler in stack has instructed not to propagate any more events */
|
|
if (stack.parent() && stack.parent().propagateEvent === false) {
|
|
stack.propagateEvent = false;
|
|
return self;
|
|
}
|
|
/* Call handler */
|
|
stack.callback();
|
|
}
|
|
/* Returns self */
|
|
return self;
|
|
};
|
|
/* Event name */
|
|
var eventName = (self.options.mode !== 'pushState' && self.options.env !== 'server') ? 'hashchange' : 'navigate';
|
|
/* Invoke when route is defined, and once again when app navigates */
|
|
return invoke().on(eventName, invoke);
|
|
};
|
|
/**
|
|
* Fire an event listener
|
|
* @param {String} event name
|
|
* @param {Mixed} [attributes] Parameters that will be applied to event handler
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.prototype.trigger = function (event) {
|
|
var self = this, params = Array.prototype.slice.call(arguments, 1);
|
|
/* Call matching events */
|
|
if (this.events[event]) {
|
|
MyRouter._forEach(this.events[event], function (fn) {
|
|
fn.apply(self, params);
|
|
});
|
|
}
|
|
return this;
|
|
};
|
|
/**
|
|
* Add an event listener
|
|
* @param {String} event name (multiple events can be called when separated by a space " ")
|
|
* @param {Function} handler - callback
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.prototype.on = MyRouter.prototype.bind = function (event, handler) {
|
|
var self = this, events = event.split(' ');
|
|
MyRouter._forEach(events, function (event) {
|
|
if (self.events[event]) {
|
|
self.events[event].push(handler);
|
|
} else {
|
|
self.events[event] = [handler];
|
|
}
|
|
});
|
|
return this;
|
|
};
|
|
/**
|
|
* Allow event to be called only once
|
|
* @param {String} event name(s)
|
|
* @param {Function} handler - callback
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.prototype.once = function (event, handler) {
|
|
var ran = false;
|
|
return this.on(event, function () {
|
|
if (ran) {
|
|
return false;
|
|
}
|
|
ran = true;
|
|
handler.apply(this, arguments);
|
|
handler = null;
|
|
return true;
|
|
});
|
|
};
|
|
/**
|
|
* @param {String} context - Route context (without trailing slash)
|
|
* param {[Function]} Middleware (optional)
|
|
* @return {Function} Adds route to context
|
|
*/
|
|
MyRouter.prototype.context = function (context) {
|
|
var self = this, middleware = Array.prototype.slice.call(arguments, 1);
|
|
return function () {
|
|
var value = arguments[0],
|
|
submiddleware = (arguments.length > 2) ? Array.prototype.slice.call(arguments, 1, -1) : [],
|
|
handler = Array.prototype.slice.call(arguments, -1)[0],
|
|
prefix = (context.slice(-1) !== '/' && value !== '/' && value !== '') ? context + '/' : context,
|
|
path = (value.substr(0, 1) !== '/') ? value : value.substr(1),
|
|
pattern = prefix + path;
|
|
return self.add.apply(self, [pattern].concat(middleware).concat(submiddleware).concat([handler]));
|
|
};
|
|
};
|
|
/**
|
|
* Navigate through history API
|
|
* @param {String} path - Pathname
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.prototype.navigate = function (path) {
|
|
return this.path(path).trigger('navigate');
|
|
};
|
|
MyRouter.prototype.path = function (pathname) {
|
|
var self = this, frag;
|
|
if ('string' === typeof pathname) {
|
|
/* Set path */
|
|
if (self.options.mode === 'pushState') {
|
|
frag = (self.options.root) ? (self.options.root + pathname) : pathname;
|
|
root.history.pushState({}, null, frag);
|
|
} else if (root.location) {
|
|
root.location.hash = (self.options.hashBang ? '!' : '') + pathname;
|
|
} else {
|
|
root._pathname = pathname || '';
|
|
}
|
|
return this;
|
|
} else if ('undefined' === typeof pathname) {
|
|
/* Get path */
|
|
if (self.options.mode === 'pushState') {
|
|
frag = root.location.pathname.replace(self.options.root, '');
|
|
} else if (self.options.mode !== 'pushState' && root.location) {
|
|
frag = (root.location.hash) ? root.location.hash.split((self.options.hashBang ? '#!' : '#'))[1] : '';
|
|
} else {
|
|
frag = root._pathname || '';
|
|
}
|
|
return frag;
|
|
} else if (pathname === false) {
|
|
/* Clear path */
|
|
if (self.options.mode === 'pushState') {
|
|
root.history.pushState({}, null, self.options.root || '/');
|
|
} else if (root.location) {
|
|
root.location.hash = (self.options.hashBang) ? '!' : '';
|
|
}
|
|
return self;
|
|
}
|
|
};
|
|
/**
|
|
* Create routes based on an object
|
|
* @param {Object} [Options, Routes]
|
|
* @param {Object Routes}
|
|
* @return {self} Router
|
|
*/
|
|
MyRouter.listen = function () {
|
|
var opts, routes;
|
|
if (arguments[0] && arguments[1]) {
|
|
opts = arguments[0];
|
|
routes = arguments[1];
|
|
} else {
|
|
routes = arguments[0];
|
|
}
|
|
/* Return a new MyRouter instance */
|
|
return (function () {
|
|
/* TODO: Accept multi-level routes */
|
|
for (var key in routes) {
|
|
this.add.call(this, key, routes[key]);
|
|
}
|
|
return this;
|
|
}).call(new MyRouter(opts || {}));
|
|
};
|
|
/**
|
|
* Create a call stack that can be enqueued by handlers and middleware
|
|
* @param {Object} router - Router
|
|
* @param {Object} extendObj - Extend
|
|
* @return {self} CallStack
|
|
*/
|
|
class CallStack {
|
|
constructor(router, extendObj) {
|
|
this.stack = CallStack.global.slice(0);
|
|
this.router = router;
|
|
this.runCallback = true;
|
|
this.callbackRan = false;
|
|
this.propagateEvent = true;
|
|
this.value = router.path();
|
|
for (var key in extendObj) {
|
|
this[key] = extendObj[key];
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
/**
|
|
* Build request parameters and allow them to be checked against a string (usually the current path)
|
|
* @param {String} route - Route
|
|
* @return {self} Request
|
|
*/
|
|
function Request(route) {
|
|
this.route = route;
|
|
this.keys = [];
|
|
this.regex = MyRouter.regexRoute(route, this.keys);
|
|
}
|
|
;
|
|
/* This allows global middleware */
|
|
CallStack.global = [];
|
|
/**
|
|
* Prevent a callback from being called
|
|
* @return {self} CallStack
|
|
*/
|
|
CallStack.prototype.preventDefault = function () {
|
|
this.runCallback = false;
|
|
};
|
|
/**
|
|
* Prevent any future callbacks from being called
|
|
* @return {self} CallStack
|
|
*/
|
|
CallStack.prototype.stopPropagation = function () {
|
|
this.propagateEvent = false;
|
|
};
|
|
/**
|
|
* Get parent state
|
|
* @return {Object} Previous state
|
|
*/
|
|
CallStack.prototype.parent = function () {
|
|
var hasParentEvents = !!(this.previousState && this.previousState.value && this.previousState.value == this.value);
|
|
return (hasParentEvents) ? this.previousState : false;
|
|
};
|
|
/**
|
|
* Run a callback (calls to next)
|
|
* @return {self} CallStack
|
|
*/
|
|
CallStack.prototype.callback = function () {
|
|
this.callbackRan = true;
|
|
this.timeStamp = Date.now();
|
|
this.next();
|
|
};
|
|
/**
|
|
* Add handler or middleware to the stack
|
|
* @param {Function|Array} handler - Handler or a array of handlers
|
|
* @param {Int} atIndex - Index to start inserting
|
|
* @return {self} CallStack
|
|
*/
|
|
CallStack.prototype.enqueue = function (handler, atIndex) {
|
|
var handlers = (!Array.isArray(handler)) ? [handler] : ((atIndex < handler.length) ? handler.reverse() : handler);
|
|
while (handlers.length) {
|
|
this.stack.splice(atIndex || this.stack.length + 1, 0, handlers.shift());
|
|
}
|
|
return this;
|
|
};
|
|
/**
|
|
* Call to next item in stack -- this adds the `req`, `event`, and `next()` arguments to all middleware
|
|
* @return {self} CallStack
|
|
*/
|
|
CallStack.prototype.next = function () {
|
|
var self = this;
|
|
return this.stack.shift().call(this.router, this.req, this, function next() {
|
|
self.next.call(self);
|
|
});
|
|
};
|
|
/**
|
|
* Match a path string -- returns a request object if there is a match -- returns false otherwise
|
|
* @param {String} path
|
|
* @return {Object} req
|
|
*/
|
|
Request.prototype.parse = function (path) {
|
|
var match = path.match(this.regex), self = this;
|
|
var req = {params: {}, keys: this.keys, matches: (match || []).slice(1), match: match};
|
|
/* Build parameters */
|
|
MyRouter._forEach(req.matches, function (value, i) {
|
|
var key = (self.keys[i] && self.keys[i].name) ? self.keys[i].name : i;
|
|
/* Parameter key will be its key or the iteration index. This is useful if a wildcard (*) is matched */
|
|
req.params[key] = (value) ? decodeURIComponent(value) : undefined;
|
|
});
|
|
return req;
|
|
};
|
|
|
|
MyRouter.CallStack = CallStack;
|
|
MyRouter.Request = Request;
|
|
tts.Router = new MyRouter();
|
|
|
|
/*
|
|
* End of MyRouter Router
|
|
*/
|
|
|