137 lines
3.1 KiB
JavaScript

import { isFunction, isUndefined, Null, ObjectKeys } from 'trimkit';
function nop() {}
export function Router(baseUrl, routes, unmatched) {
let listening = false;
let currentRoute = Null;
let reEnterHook = Null;
const VARMATCH_RE = /:([^\/]+)/g;
const ROUTEELEMENT_RE = /^[^\/]+$/;
const routeMatcher = ObjectKeys(routes).map(name => {
const route = routes[name];
const reg = route.path.replace(VARMATCH_RE, (m, varName) => {
const varDef = route.vars || {};
const regExStr = '' + (varDef[varName] || /[^\/]+/);
return '(' + regExStr.substring(1, regExStr.lastIndexOf('/')) + ')';
});
return {
matcher: new RegExp(`^${baseUrl}${reg}$`),
name,
...route
};
});
function goto(urlOrName, vars) {
const url = urlOrName.startsWith(baseUrl) ? urlOrName : href(urlOrName, vars);
if (listening && _location() !== url) {
// If we're not there make the change and exit.
location.hash = url;
return;
}
if (currentRoute && currentRoute.path === url) {
// This is only supposed to happen when recovering from a bad path.
return;
}
let routeVars = {};
const routeMatch = routeMatcher.find(({ matcher, path }) => {
const match = url.match(matcher);
if (!match) {
return false;
}
if (path.indexOf(':') !== -1) {
match.shift();
path.replace(VARMATCH_RE, (_, varName) => {
// We're abusing RegExp.replace here.
routeVars[varName] = match.shift();
});
}
return true;
});
if (routeMatch) {
let newRoute = {
name: routeMatch.name,
vars: routeVars,
path: url
};
if (currentRoute && currentRoute.name === newRoute.name && isFunction(reEnterHook)) {
reEnterHook(newRoute);
currentRoute = newRoute;
} else {
let onexit = currentRoute && currentRoute.name ? routes[currentRoute.name].onexit || nop : nop;
Promise.resolve(onexit(api, currentRoute, newRoute)).then(() => {
reEnterHook = routes[routeMatch.name].onenter(api, newRoute);
currentRoute = newRoute;
});
}
} else if (unmatched) {
unmatched(api, url, currentRoute);
} else {
if (currentRoute && listening) {
location.hash = currentRoute.path;
return;
}
throw new Error(`No route for "${url}"`);
}
}
function href(routeName, vars) {
const route = routes[routeName];
if (!route) {
throw new Error(`Invalid route ${routeName}.`);
}
let path = '' + route.path;
if (route.vars) {
path = path.replace(VARMATCH_RE, (_, varName) => {
let value = vars[varName];
if ((route.vars[varName] || ROUTEELEMENT_RE).test(value)) {
value = isUndefined(value) ? '' : '' + value;
return value;
}
throw new Error(`Invalid value for route ${path} var ${varName}: ${value}.`);
});
}
return `${baseUrl}${path}`;
}
function _location() {
return location.hash;
}
function listen(initialRoute) {
if (listening) {
return;
}
self.addEventListener('hashchange', () => goto(_location()), false);
listening = true;
goto(_location() || initialRoute);
}
function current() {
return currentRoute;
}
const api = {
goto,
href,
listen,
current
};
return api;
}