137 lines
3.1 KiB
JavaScript
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;
|
|
}
|