"use strict"; import EventEmitter from 'event-emitter-es6'; import pathspec_to_regex from '../Shared/Pathspec.mjs'; /** * Client-side request router. * 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. */ 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)); } /** * 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}'.`)); */ add_404(callback) { this.callback_404 = callback; } /** * 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.`)); */ add_page(routespec, callback) { this.routes.push({ spec: routespec, match: routespec instanceof RegExp ? { regex: routespec, tokens: [] } : pathspec_to_regex(routespec), callback }); } /** * Manually navigate to a given path. * @param {string} path The path to navigate to. * @example router.navigate("/add/carrot/frederick/10001"); */ 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); } /** * Navigate to the current URL's hash value - i.e. the bit after the '#' symbol in the URL. * @example router.navigate_current_hash(); */ navigate_current_hash() { this.navigate(window.location.hash.substr(1)); } } export default ClientRouter;