vicowa-web-components
Version:
199 lines (177 loc) • 5.72 kB
JavaScript
const privateData = Symbol('privateData');
function createRoute(p_Route, p_Callbacks) {
p_Route = p_Route.replace(/[/]+/g, '/');
const pathParts = p_Route.split('/');
const destinations = pathParts.map((part) => {
const props = {};
if (/^:/.test(part)) {
if (/\(\.\*\)$/.test(part)) {
props.name = part.substring(1, part.length - 4);
props.all = true;
} else if (/\?$/.test(part)) {
props.name = part.substring(1, part.length - 1);
props.optional = true;
} else {
props.name = part.substring(1);
}
} else if (/\*/.test(part)) {
if (/^\*/.test(part)) {
props.all = true;
} else {
props.wildcard = true;
props.regExp = new RegExp(`^${part.replace(/\./g, '\\.').replace('*', '.*')}$`);
}
} else if (!part) {
props.slash = true;
} else {
props.regExp = new RegExp(`^${part}$`);
}
return props;
});
return {
destinations,
callbacks: p_Callbacks,
};
}
function handleChangeLocation(p_RouterData, p_TargetWindow) {
if (p_RouterData.url && (!p_TargetWindow.history.state || p_RouterData.url !== p_TargetWindow.history.state.url)) {
if (!p_TargetWindow.history.state) {
p_TargetWindow.history.replaceState({ url: p_RouterData.url, customData: p_RouterData.customData }, p_RouterData.title, p_RouterData.url);
}
p_TargetWindow.history.pushState({ url: p_RouterData.url, customData: p_RouterData.customData }, p_RouterData.title, p_RouterData.url);
}
}
function handleRoute(p_RouterData, p_Url, p_CustomData) {
const { routes, notFoundHandler, targetWindow } = p_RouterData;
const regExp = new RegExp(`^${document.location.origin}`);
p_Url = p_Url.replace(regExp, '').replace(/[/]+/g, '/');
const queryParts = p_Url.split('?');
const urlParts = queryParts[0].split('/');
let query;
const route = routes.find((testRoute) => testRoute.destinations.every((destination, index, destArray) => {
let result = false;
if (destination.slash) {
result = urlParts[index] === '';
} else if (destination.regExp) {
result = destination.regExp.test(urlParts[index]);
} else if (destination.optional) {
result = urlParts.length === index || urlParts.length === index - 1;
} else if (destination.all) {
result = urlParts.length - 1 >= index;
} else if (destination.name) {
result = (index === destArray.length - 1 && urlParts.length === destArray.length) || (urlParts.length - 1 > index && destArray.length - 1 > index);
} else {
result = urlParts.length - 1 === index;
}
return result;
}));
if (queryParts.length > 1) {
const parts = queryParts[1].split('&');
query = parts.reduce((previous, current) => {
const subParts = current.split('=').map((subPart) => subPart.trim());
previous[subParts[0]] = subParts[1];
return previous;
}, {});
}
if (route) {
const context = route.destinations.reduce((previous, routePart, index) => {
if (urlParts.length) {
if (index < route.destinations.length - 1) {
const value = urlParts.shift();
if (routePart.name) {
previous.params[routePart.name] = value;
}
} else {
previous.params[routePart.name || 0] = urlParts.join('/');
}
}
return previous;
}, { params: {}, url: p_Url, query, customData: p_CustomData });
const callbacks = [...route.callbacks];
const doCallback = async(nextCallback) => {
if (nextCallback) {
if (nextCallback.length > 1) {
await nextCallback(context, async() => {
if (callbacks.length) {
await doCallback(callbacks.shift());
if (!callbacks.length) {
handleChangeLocation(context, targetWindow);
}
}
});
} else {
await nextCallback(context);
handleChangeLocation(context, targetWindow);
}
}
};
doCallback(callbacks.shift());
} else if (notFoundHandler) {
// do 404 here
notFoundHandler({
url: p_Url,
query,
customData: p_CustomData,
});
}
}
class Router {
constructor(p_TargetWindow = window) {
this[privateData] = {
routes: [],
notFoundHandler: undefined,
targetWindow: p_TargetWindow,
};
const handleLoadState = (p_State) => {
const routerData = this[privateData];
delete routerData.url;
delete routerData.title;
if (p_State && p_State.url) {
routerData.title = p_State.title;
routerData.url = p_State.url;
routerData.customData = p_State.customData;
} else if (routerData.targetWindow.history.state) {
routerData.title = routerData.targetWindow.history.state.title;
routerData.url = routerData.targetWindow.history.state.url;
routerData.customData = routerData.targetWindow.history.state.customData;
}
if (routerData.url) {
handleRoute(routerData, routerData.url, routerData.customData);
} else if (document.location.href) {
handleRoute(routerData, document.location.href, null);
}
};
handleLoadState(p_TargetWindow.history.state);
p_TargetWindow.addEventListener('popstate', (p_Event) => {
handleLoadState(p_Event.state);
});
p_TargetWindow.addEventListener('load', () => {
this.goTo(document.location.href);
});
}
set onNotFound(p_Handler) {
this[privateData].notFoundHandler = p_Handler;
}
get onNotFound() {
return this[privateData].notFoundHandler;
}
addRoute(p_Route, ...p_Callbacks) {
this[privateData].routes.push(createRoute(p_Route, p_Callbacks));
}
goTo(p_Url, p_CustomData) {
handleRoute(this[privateData], p_Url, p_CustomData);
}
clearRoutes() {
this[privateData].routes = [];
}
}
const routers = new Map();
export function getRouter(p_Window) {
if (!routers.has(p_Window)) {
routers.set(p_Window, new Router(p_Window));
}
return routers.get(p_Window);
}
export function removeRouter(p_Window) {
routers.delete(p_Window);
}