import { select, element, Selection } from './selection' import { zip } from './utils' interface RouterPage { cleanUp?(): void } interface RouteOptions { [key: string]: string } type RouterHandler = (params: RouterParams) => Selection interface RouterParams { [param: string]: string } interface RouterRoutes { [route: string]: RouterHandler } interface RouterOptions { routes: RouterRoutes class?: string processPath?: (path: string) => string | undefined } interface RouterApi { navigateTo (path: string): void } function defaultProcessPath (path: string): string { return path } interface PageHandler { parts: Array handler: RouterHandler } function getHandlerForPath (routes: RouterRoutes, path: string): PageHandler { const paths = Object.keys(routes).map(function (path) { return { parts: path.split('/').slice(1), handler: routes[path] } }) const requestParts = path.split('/').slice(1) return paths.find(({ parts }) => { if (parts.length !== requestParts.length) { return false } return zip(parts, requestParts).every(([tp, rp]) => tp[0] === ':' || tp === rp) }) } function canDisplay (routes: RouterRoutes, path: string) { return getHandlerForPath(routes, path) !== undefined } function getLinkParent (element: Node): HTMLAnchorElement | undefined { let node: Node = element while (node !== null) { if (node instanceof HTMLAnchorElement && node.href) { return node } node = node.parentNode } return undefined } function router (options: RouterOptions): Selection { const mount = element({ class: { 'cx-router': true, [options.class]: !!options.class } }) const processPath = options.processPath || defaultProcessPath let mounted: Selection = undefined let current = processPath(document.location.pathname) function display (path: string): void { current = path const requestParts = path.split('/').slice(1) const { parts, handler } = getHandlerForPath(options.routes, path) const parsed: RouteOptions = {} parts.map(function (part, i) { if (part[0] === ':') { parsed[part.slice(1)] = requestParts[i] } }) if (mounted) { const oldApi = mounted.api() if (oldApi && oldApi.cleanUp) { oldApi.cleanUp() } } mounted = handler(parsed) mount.set(mounted) } select('body').on('click', (event: Event) => { const node = getLinkParent(event.target as Node) // if it is a left click on an . // TODO: handle ctrl-click if (node && ((event instanceof MouseEvent && event.button === 0) || (event instanceof TouchEvent))) { // if it's the same origin, then we are staying in the same site, so we carry on with the checks if (node.origin === document.location.origin) { const newPath = processPath(node.pathname) if (current !== newPath && canDisplay(options.routes, newPath)) { event.preventDefault() display(newPath) window.history.pushState(null, '', node.href) } } } }) window.onpopstate = () => { const newPath = processPath(document.location.pathname) if (newPath !== current) { display(processPath(newPath)) } } if (canDisplay(options.routes, current)) { display(current) } const api = { navigateTo (path: string): void { const newPath = processPath(path) if (canDisplay(options.routes, newPath)) { display(newPath) window.history.pushState(null, '', path) } } } mount.api(api) return mount } export { RouterApi, RouterOptions, RouterRoutes, RouterParams, RouterHandler, RouterPage, router }