|  | // Copyright (C) 2018 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import * as m from 'mithril'; | 
|  |  | 
|  | import {Actions, DeferredAction} from '../common/actions'; | 
|  | import {Analytics} from '../frontend/analytics'; | 
|  |  | 
|  | interface RouteMap { | 
|  | [route: string]: m.Component; | 
|  | } | 
|  |  | 
|  | export const ROUTE_PREFIX = '#!'; | 
|  |  | 
|  | export class Router { | 
|  | constructor( | 
|  | private defaultRoute: string, private routes: RouteMap, | 
|  | private dispatch: (a: DeferredAction) => void, | 
|  | private logging: Analytics) { | 
|  | if (!(defaultRoute in routes)) { | 
|  | throw Error('routes must define a component for defaultRoute.'); | 
|  | } | 
|  |  | 
|  | window.onhashchange = () => this.navigateToCurrentHash(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Parses and returns the current route string from |window.location.hash|. | 
|  | * May return routes that are not defined in |this.routes|. | 
|  | */ | 
|  | getRouteFromHash(): string { | 
|  | const prefixLength = ROUTE_PREFIX.length; | 
|  | const hash = window.location.hash; | 
|  |  | 
|  | // Do not try to parse route if prefix doesn't match. | 
|  | if (hash.substring(0, prefixLength) !== ROUTE_PREFIX) return ''; | 
|  |  | 
|  | return hash.split('?')[0].substring(prefixLength); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sets |route| on |window.location.hash|. If |route| if not defined in | 
|  | * |this.routes|, dispatches a navigation to |this.defaultRoute|. | 
|  | */ | 
|  | setRouteOnHash(route: string) { | 
|  | history.pushState(undefined, "", ROUTE_PREFIX + route); | 
|  | this.logging.updatePath(route); | 
|  |  | 
|  | if (!(route in this.routes)) { | 
|  | console.info( | 
|  | `Route ${route} not known redirecting to ${this.defaultRoute}.`); | 
|  | this.dispatch(Actions.navigate({route: this.defaultRoute})); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Dispatches navigation action to |this.getRouteFromHash()| if that is | 
|  | * defined in |this.routes|, otherwise to |this.defaultRoute|. | 
|  | */ | 
|  | navigateToCurrentHash() { | 
|  | const hashRoute = this.getRouteFromHash(); | 
|  | const newRoute = hashRoute in this.routes ? hashRoute : this.defaultRoute; | 
|  | this.dispatch(Actions.navigate({route: newRoute})); | 
|  | // TODO(dproy): Handle case when new route has a permalink. | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the component for given |route|. If |route| is not defined, returns | 
|  | * component of |this.defaultRoute|. | 
|  | */ | 
|  | resolve(route: string|null): m.Component { | 
|  | if (!route || !(route in this.routes)) { | 
|  | return this.routes[this.defaultRoute]; | 
|  | } | 
|  | return this.routes[route]; | 
|  | } | 
|  |  | 
|  | static param(key: string) { | 
|  | const hash = window.location.hash; | 
|  | const paramStart = hash.indexOf('?'); | 
|  | if (paramStart === -1) return undefined; | 
|  | return m.parseQueryString(hash.substring(paramStart))[key]; | 
|  | } | 
|  | } |