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