154 lines
3.6 KiB
JavaScript
154 lines
3.6 KiB
JavaScript
import { isFunction, isUndefined, Null, ObjectKeys } from '../node_modules/trimkit/src/index.js';
|
|
|
|
const VARMATCH_RE = /:([^\/]+)/g;
|
|
const ROUTEELEMENT_RE = /^[^\/]+$/;
|
|
const nop = () => 1;
|
|
const digestRoutes = (routes, baseUrl) =>
|
|
routes.map((route, i) => {
|
|
const reg = route.path.replace(VARMATCH_RE, (m, varName) => {
|
|
const varDef = route.vars || {};
|
|
const regExStr = '' + (varDef[varName] || /[^\/]+/);
|
|
return '(' + regExStr.substring(1, regExStr.lastIndexOf('/')) + ')';
|
|
});
|
|
|
|
return Object.assign(
|
|
{
|
|
matcher: new RegExp(`^${baseUrl}${reg}$`),
|
|
_i: i
|
|
},
|
|
route
|
|
);
|
|
});
|
|
|
|
export function Router(routes, baseUrl = '#') {
|
|
let listening = false;
|
|
let currentRoute = Null;
|
|
let reEnterHook = Null;
|
|
|
|
let routeMatcher = digestRoutes(routes, baseUrl);
|
|
let routeByName = routeMatcher.reduce(
|
|
(obj, route) => (route.name ? Object.assign(obj, { [route.name]: route }) : obj),
|
|
{}
|
|
);
|
|
|
|
function goto(urlOrName, vars, fromLocation) {
|
|
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 and it's awesome!
|
|
routeVars[varName] = match.shift();
|
|
});
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (routeMatch) {
|
|
let newRoute = {
|
|
name: routeMatch.name,
|
|
vars: routeVars,
|
|
path: url,
|
|
_i: routeMatch._i
|
|
};
|
|
if (currentRoute && currentRoute._i === newRoute._i && isFunction(reEnterHook)) {
|
|
const result = reEnterHook(newRoute);
|
|
currentRoute = newRoute;
|
|
return Promise.resolve(result);
|
|
} else {
|
|
let exit = currentRoute && currentRoute._i ? routes[currentRoute._i].exit || nop : nop;
|
|
return Promise.resolve(exit(api, currentRoute, newRoute))
|
|
.catch(() => {})
|
|
.then(() => {
|
|
reEnterHook = routes[routeMatch._i].enter(api, newRoute);
|
|
currentRoute = newRoute;
|
|
});
|
|
}
|
|
} else if (currentRoute && fromLocation) {
|
|
// If we are listening and we receive an unmatched path, go back.
|
|
location.hash = currentRoute.path;
|
|
return;
|
|
}
|
|
// Either we received a goto call or a start call to in invalid path.
|
|
throw new Error(`No route for "${url}"`);
|
|
}
|
|
|
|
function href(routeName, vars) {
|
|
const route = routeByName[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 _handler() {
|
|
goto(_location(), Null, true);
|
|
}
|
|
|
|
function start(initialRoute) {
|
|
if (listening) {
|
|
return;
|
|
}
|
|
|
|
self.addEventListener('hashchange', _handler, false);
|
|
listening = true;
|
|
goto(_location() || initialRoute);
|
|
}
|
|
|
|
function stop() {
|
|
self.removeEventListener('hashchange', _handler);
|
|
}
|
|
|
|
function current() {
|
|
return currentRoute;
|
|
}
|
|
|
|
const api = {
|
|
goto,
|
|
href,
|
|
start,
|
|
stop,
|
|
current
|
|
};
|
|
|
|
return api;
|
|
}
|