2019-04-26 22:35:10 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
import EventEmitter from 'event-emitter-es6';
|
|
|
|
|
2019-04-26 23:38:02 +00:00
|
|
|
import { pathspec_to_regex } from '../Shared/Pathspec.mjs';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Client-side request router.
|
2019-04-27 14:27:09 +00:00
|
|
|
* You should use this in a browser. If you're not in a browser, you probably want ServerRouter instead.
|
|
|
|
* @extends EventEmitter
|
|
|
|
* @param {object} options The options object to use when creating the router.
|
|
|
|
* @property {Boolean} verbose Whether to be verbose and log debugging information to the console.
|
|
|
|
* @property {Boolean} listen_pushstate Whether to listen to the browser's `pushstate` event and automatically navigate on recieving it.
|
2019-04-26 23:38:02 +00:00
|
|
|
*/
|
2019-04-26 22:35:10 +00:00
|
|
|
class ClientRouter extends EventEmitter {
|
|
|
|
constructor(options) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
/***** Settings *****/
|
|
|
|
this.listen_pushstate = true;
|
|
|
|
this.verbose = false;
|
|
|
|
|
|
|
|
/********************/
|
|
|
|
|
|
|
|
for(let key in options) {
|
|
|
|
this[key] = options[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
/********************/
|
|
|
|
|
|
|
|
/** Whether we should handle popstate events or not. @type {Boolean} */
|
|
|
|
this.handle_popstates = true;
|
|
|
|
this.routes = [];
|
|
|
|
|
|
|
|
window.addEventListener("popstate", ((event) => {
|
|
|
|
if(!this.handle_popstates) return;
|
|
|
|
|
|
|
|
console.info(`[ClientRouter] Handling popstate - path: ${window.location.hash.substr(1)} (state`, event.state, `)`);
|
|
|
|
this.navigate(window.location.hash.substr(1));
|
|
|
|
}).bind(this));
|
|
|
|
}
|
|
|
|
|
2019-04-26 23:38:02 +00:00
|
|
|
/**
|
|
|
|
* Sets the function to execute when no other route could be matched.
|
|
|
|
* @param {Function} callback The callback to execute.
|
|
|
|
* @example router.add_404((path) => console.log(`Oops! Couldn't find a route to handle '${path}'.`));
|
|
|
|
*/
|
2019-04-26 22:35:10 +00:00
|
|
|
add_404(callback) {
|
|
|
|
this.callback_404 = callback;
|
|
|
|
}
|
|
|
|
|
2019-04-26 23:38:02 +00:00
|
|
|
/**
|
|
|
|
* Adds a route to the router.
|
|
|
|
* @param {string|RegExp} routespec The route specification that the route should match against. May contain regular expression syntax, and the domain-specific :param syntax. A raw regular expression may also be passed, if you need the flexibility.
|
|
|
|
* @param {Function} callback The callback to execute when the route is matched.
|
|
|
|
* @example router.add_page("/add/vegetable/:name/:weight", (params) => console.log(`We added a ${params.name} with a weight of ${params.weight}g.`));
|
|
|
|
*/
|
2019-04-26 22:35:10 +00:00
|
|
|
add_page(routespec, callback) {
|
|
|
|
this.routes.push({
|
|
|
|
spec: routespec,
|
2019-04-26 23:38:02 +00:00
|
|
|
match: pathspec instanceof RegExp ? { regex: routespec, tokens: [] } : pathspec_to_regex(routespec),
|
2019-04-26 22:35:10 +00:00
|
|
|
callback
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-04-26 23:38:02 +00:00
|
|
|
/**
|
|
|
|
* Manually navigate to a given path.
|
|
|
|
* @param {string} path The path to navigate to.
|
|
|
|
* @example router.navigate("/add/carrot/frederick/10001");
|
|
|
|
*/
|
2019-04-26 22:35:10 +00:00
|
|
|
navigate(path) {
|
|
|
|
for(let route_info of this.routes) {
|
|
|
|
const matches = path.match(route_info.match.regex);
|
|
|
|
if(matches) {
|
|
|
|
if(this.verbose) console.log(`%c[Router] Matched against ${route_info.match.regex}!`, "color: hsl(331, 76%, 40%); font-weight: bold;");
|
|
|
|
|
|
|
|
// Build the parameters object
|
|
|
|
let params = {};
|
|
|
|
for(let i = 1; i < matches.length; i++) { // Skip the top-level group match
|
|
|
|
params[route_info.match.tokens[i-1]] = matches[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't handle any popstates potentially generated by changing the hash
|
|
|
|
this.handle_popstates = false;
|
|
|
|
window.location.hash = `#${path}`;
|
|
|
|
this.handle_popstates = true;
|
|
|
|
|
|
|
|
route_info.callback(params);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this.verbose) console.debug(`%c[Router] No match against ${route_info.match.regex} - moving on.`, "color: hsl(331, 76%, 40%)");
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this.verbose) console.warn(`Couldn't find a match for '${path}'.`);
|
|
|
|
if(typeof this.callback_404 == "function")
|
|
|
|
this.callback_404(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-26 23:38:02 +00:00
|
|
|
* Navigate to the current URL's hash value - i.e. the bit after the '#' symbol in the URL.
|
|
|
|
* @example router.navigate_current_hash();
|
2019-04-26 22:35:10 +00:00
|
|
|
*/
|
2019-04-26 23:38:02 +00:00
|
|
|
navigate_current_hash() {
|
|
|
|
this.navigate(window.location.hash.substr(1));
|
2019-04-26 22:35:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ClientRouter;
|