| // Copyright 2017 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | import {assert} from 'chrome://resources/js/assert.js'; | 
 |  | 
 |  | 
 | /** | 
 |  * The different pages that can be shown at a time. | 
 |  * Note: This must remain in sync with the page ids in manager.html! | 
 |  */ | 
 | export enum Page { | 
 |   LIST = 'items-list', | 
 |   DETAILS = 'details-view', | 
 |   ACTIVITY_LOG = 'activity-log', | 
 |   SITE_PERMISSIONS = 'site-permissions', | 
 |   SITE_PERMISSIONS_ALL_SITES = 'site-permissions-by-site', | 
 |   SHORTCUTS = 'keyboard-shortcuts', | 
 |   ERRORS = 'error-page', | 
 | } | 
 |  | 
 | export enum Dialog { | 
 |   OPTIONS = 'options', | 
 | } | 
 |  | 
 | export interface PageState { | 
 |   page: Page; | 
 |   extensionId?: string; | 
 |   subpage?: Dialog; | 
 | } | 
 |  | 
 | type Listener = (pageState: PageState) => void; | 
 |  | 
 | /** @return Whether a and b are equal. */ | 
 | function isPageStateEqual(a: PageState, b: PageState): boolean { | 
 |   return a.page === b.page && a.subpage === b.subpage && | 
 |       a.extensionId === b.extensionId; | 
 | } | 
 |  | 
 | /** | 
 |  * Regular expression that captures the leading slash, the content and the | 
 |  * trailing slash in three different groups. | 
 |  */ | 
 | const CANONICAL_PATH_REGEX: RegExp = /(^\/)([\/-\w]+)(\/$)/; | 
 |  | 
 | /** | 
 |  * A helper object to manage in-page navigations. Since the extensions page | 
 |  * needs to support different urls for different subpages (like the details | 
 |  * page), we use this object to manage the history and url conversions. | 
 |  */ | 
 | export class NavigationHelper { | 
 |   private nextListenerId_: number = 1; | 
 |   private listeners_: Map<number, Listener> = new Map(); | 
 |   private previousPage_?: PageState; | 
 |  | 
 |   constructor() { | 
 |     this.processRoute_(); | 
 |  | 
 |     window.addEventListener('popstate', () => { | 
 |       this.notifyRouteChanged_(this.getCurrentPage()); | 
 |     }); | 
 |   } | 
 |  | 
 |   private get currentPath_(): string { | 
 |     return location.pathname.replace(CANONICAL_PATH_REGEX, '$1$2'); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Going to /configureCommands and /shortcuts should land you on /shortcuts, | 
 |    * and going to /sitePermissions should land you on /sitePermissions. | 
 |    * These are the only three supported routes, so all other cases will redirect | 
 |    * you to root path if not already on it. | 
 |    */ | 
 |   private processRoute_() { | 
 |     if (this.currentPath_ === '/configureCommands' || | 
 |         this.currentPath_ === '/shortcuts') { | 
 |       window.history.replaceState( | 
 |           undefined /* stateObject */, '', '/shortcuts'); | 
 |     } else if (this.currentPath_ === '/sitePermissions') { | 
 |       window.history.replaceState( | 
 |           undefined /* stateObject */, '', '/sitePermissions'); | 
 |     } else if (this.currentPath_ === '/sitePermissions/allSites') { | 
 |       window.history.replaceState( | 
 |           undefined /* stateObject */, '', '/sitePermissions/allSites'); | 
 |     } else if (this.currentPath_ !== '/') { | 
 |       window.history.replaceState(undefined /* stateObject */, '', '/'); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return The page that should be displayed for the current URL. | 
 |    */ | 
 |   getCurrentPage(): PageState { | 
 |     const search = new URLSearchParams(location.search); | 
 |     let id = search.get('id'); | 
 |     if (id) { | 
 |       return {page: Page.DETAILS, extensionId: id}; | 
 |     } | 
 |     id = search.get('activity'); | 
 |     if (id) { | 
 |       return {page: Page.ACTIVITY_LOG, extensionId: id}; | 
 |     } | 
 |     id = search.get('options'); | 
 |     if (id) { | 
 |       return {page: Page.DETAILS, extensionId: id, subpage: Dialog.OPTIONS}; | 
 |     } | 
 |     id = search.get('errors'); | 
 |     if (id) { | 
 |       return {page: Page.ERRORS, extensionId: id}; | 
 |     } | 
 |  | 
 |     if (this.currentPath_ === '/shortcuts') { | 
 |       return {page: Page.SHORTCUTS}; | 
 |     } | 
 |     if (this.currentPath_ === '/sitePermissions') { | 
 |       return {page: Page.SITE_PERMISSIONS}; | 
 |     } | 
 |     if (this.currentPath_ === '/sitePermissions/allSites') { | 
 |       return {page: Page.SITE_PERMISSIONS_ALL_SITES}; | 
 |     } | 
 |  | 
 |     return {page: Page.LIST}; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Function to add subscribers. | 
 |    * @param {!function(!PageState)} listener | 
 |    * @return A numerical ID to be used for removing the listener. | 
 |    */ | 
 |   addListener(listener: Listener): number { | 
 |     const nextListenerId = this.nextListenerId_++; | 
 |     this.listeners_.set(nextListenerId, listener); | 
 |     return nextListenerId; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Remove a previously registered listener. | 
 |    * @return Whether a listener with the given ID was actually found and | 
 |    *   removed. | 
 |    */ | 
 |   removeListener(id: number): boolean { | 
 |     return this.listeners_.delete(id); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Function to notify subscribers. | 
 |    */ | 
 |   private notifyRouteChanged_(newPage: PageState) { | 
 |     for (const listener of this.listeners_.values()) { | 
 |       listener(newPage); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param newPage the page to navigate to. | 
 |    */ | 
 |   navigateTo(newPage: PageState) { | 
 |     const currentPage = this.getCurrentPage(); | 
 |     if (currentPage && isPageStateEqual(currentPage, newPage)) { | 
 |       return; | 
 |     } | 
 |  | 
 |     this.updateHistory(newPage, false /* replaceState */); | 
 |     this.notifyRouteChanged_(newPage); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param newPage the page to replace the current page with. | 
 |    */ | 
 |   replaceWith(newPage: PageState) { | 
 |     this.updateHistory(newPage, true /* replaceState */); | 
 |     if (this.previousPage_ && isPageStateEqual(this.previousPage_, newPage)) { | 
 |       // Skip the duplicate history entry. | 
 |       history.back(); | 
 |       return; | 
 |     } | 
 |     this.notifyRouteChanged_(newPage); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Called when a page changes, and pushes state to history to reflect it. | 
 |    */ | 
 |   updateHistory(entry: PageState, replaceState: boolean) { | 
 |     let path; | 
 |     switch (entry.page) { | 
 |       case Page.LIST: | 
 |         path = '/'; | 
 |         break; | 
 |       case Page.ACTIVITY_LOG: | 
 |         path = '/?activity=' + entry.extensionId; | 
 |         break; | 
 |       case Page.DETAILS: | 
 |         if (entry.subpage) { | 
 |           assert(entry.subpage === Dialog.OPTIONS); | 
 |           path = '/?options=' + entry.extensionId; | 
 |         } else { | 
 |           path = '/?id=' + entry.extensionId; | 
 |         } | 
 |         break; | 
 |       case Page.SITE_PERMISSIONS: | 
 |         path = '/sitePermissions'; | 
 |         break; | 
 |       case Page.SITE_PERMISSIONS_ALL_SITES: | 
 |         path = '/sitePermissions/allSites'; | 
 |         break; | 
 |       case Page.SHORTCUTS: | 
 |         path = '/shortcuts'; | 
 |         break; | 
 |       case Page.ERRORS: | 
 |         path = '/?errors=' + entry.extensionId; | 
 |         break; | 
 |     } | 
 |     assert(path); | 
 |     const state = {url: path}; | 
 |     const currentPage = this.getCurrentPage(); | 
 |     const isDialogNavigation = currentPage.page === entry.page && | 
 |         currentPage.extensionId === entry.extensionId; | 
 |     // Navigating to a dialog doesn't visually change pages; it just opens | 
 |     // a dialog. As such, we replace state rather than pushing a new state | 
 |     // on the stack so that hitting the back button doesn't just toggle the | 
 |     // dialog. | 
 |     if (replaceState || isDialogNavigation) { | 
 |       history.replaceState(state, '', path); | 
 |     } else { | 
 |       this.previousPage_ = currentPage; | 
 |       history.pushState(state, '', path); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | export const navigation = new NavigationHelper(); |