1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {Actions, DeferredAction} from '../common/actions'; 18import {Analytics} from '../frontend/analytics'; 19 20interface RouteMap { 21 [route: string]: m.Component; 22} 23 24export const ROUTE_PREFIX = '#!'; 25 26export class Router { 27 constructor( 28 private defaultRoute: string, private routes: RouteMap, 29 private dispatch: (a: DeferredAction) => void, 30 private logging: Analytics) { 31 if (!(defaultRoute in routes)) { 32 throw Error('routes must define a component for defaultRoute.'); 33 } 34 35 window.onhashchange = () => this.navigateToCurrentHash(); 36 } 37 38 /** 39 * Parses and returns the current route string from |window.location.hash|. 40 * May return routes that are not defined in |this.routes|. 41 */ 42 getRouteFromHash(): string { 43 const prefixLength = ROUTE_PREFIX.length; 44 const hash = window.location.hash; 45 46 // Do not try to parse route if prefix doesn't match. 47 if (hash.substring(0, prefixLength) !== ROUTE_PREFIX) return ''; 48 49 return hash.split('?')[0].substring(prefixLength); 50 } 51 52 /** 53 * Sets |route| on |window.location.hash|. If |route| if not defined in 54 * |this.routes|, dispatches a navigation to |this.defaultRoute|. 55 */ 56 setRouteOnHash(route: string) { 57 history.pushState(undefined, "", ROUTE_PREFIX + route); 58 this.logging.updatePath(route); 59 60 if (!(route in this.routes)) { 61 console.info( 62 `Route ${route} not known redirecting to ${this.defaultRoute}.`); 63 this.dispatch(Actions.navigate({route: this.defaultRoute})); 64 } 65 } 66 67 /** 68 * Dispatches navigation action to |this.getRouteFromHash()| if that is 69 * defined in |this.routes|, otherwise to |this.defaultRoute|. 70 */ 71 navigateToCurrentHash() { 72 const hashRoute = this.getRouteFromHash(); 73 const newRoute = hashRoute in this.routes ? hashRoute : this.defaultRoute; 74 this.dispatch(Actions.navigate({route: newRoute})); 75 // TODO(dproy): Handle case when new route has a permalink. 76 } 77 78 /** 79 * Returns the component for given |route|. If |route| is not defined, returns 80 * component of |this.defaultRoute|. 81 */ 82 resolve(route: string|null): m.Component { 83 if (!route || !(route in this.routes)) { 84 return this.routes[this.defaultRoute]; 85 } 86 return this.routes[route]; 87 } 88 89 static param(key: string) { 90 const hash = window.location.hash; 91 const paramStart = hash.indexOf('?'); 92 if (paramStart === -1) return undefined; 93 return m.parseQueryString(hash.substring(paramStart))[key]; 94 } 95} 96