| // Copyright 2014 The Chromium Authors. All rights reserved. | 
 | // 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.m.js'; | 
 | import {addSingletonGetter} from 'chrome://resources/js/cr.m.js'; | 
 | import {FocusOutlineManager} from 'chrome://resources/js/cr/ui/focus_outline_manager.m.js'; | 
 |  | 
 | import {Page} from './page.js'; | 
 |  | 
 | /** | 
 |  * PageManager contains a list of root Page objects and handles "navigation" | 
 |  * by showing and hiding these pages. On initial load, PageManager can use | 
 |  * the path to open the correct hierarchy of pages. | 
 |  */ | 
 | export class PageManager { | 
 |   constructor() { | 
 |     /** | 
 |      * True if page is served from a dialog. | 
 |      * @type {boolean} | 
 |      */ | 
 |     this.isDialog = false; | 
 |  | 
 |     /** | 
 |      * Root pages. Maps lower-case page names to the respective page object. | 
 |      * @type {!Map<string, !Page>} | 
 |      */ | 
 |     this.registeredPages = new Map(); | 
 |  | 
 |     /** | 
 |      * Observers will be notified when opening and closing overlays. | 
 |      * @private {!Array<!PageManagerObserver>} | 
 |      */ | 
 |     this.observers_ = []; | 
 |  | 
 |     /** @private {?Page} */ | 
 |     this.defaultPage_ = null; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Initializes the complete page. | 
 |    * @param {Page} defaultPage The page to be shown when no | 
 |    *     page is specified in the path. | 
 |    */ | 
 |   initialize(defaultPage) { | 
 |     this.defaultPage_ = defaultPage; | 
 |  | 
 |     FocusOutlineManager.forDocument(document); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Registers new page. | 
 |    * @param {!Page} page Page to register. | 
 |    */ | 
 |   register(page) { | 
 |     this.registeredPages.set(page.name.toLowerCase(), page); | 
 |     page.addEventListener( | 
 |         'page-hash-changed', | 
 |         e => this.onPageHashChanged_(/** @type {!CustomEvent} */ (e))); | 
 |     page.initializePage(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Unregisters an existing page. | 
 |    * @param {!Page} page Page to unregister. | 
 |    */ | 
 |   unregister(page) { | 
 |     this.registeredPages.delete(page.name.toLowerCase()); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Shows the default page. | 
 |    * @param {boolean=} opt_updateHistory If we should update the history after | 
 |    *     showing the page (defaults to true). | 
 |    */ | 
 |   showDefaultPage(opt_updateHistory) { | 
 |     assert( | 
 |         this.defaultPage_ instanceof Page, | 
 |         'PageManager must be initialized with a default page.'); | 
 |     this.showPageByName(this.defaultPage_.name, opt_updateHistory); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Shows a registered page. | 
 |    * @param {string} pageName Page name. | 
 |    * @param {boolean=} opt_updateHistory If we should update the history after | 
 |    *     showing the page (defaults to true). | 
 |    * @param {Object=} opt_propertyBag An optional bag of properties including | 
 |    *     replaceState (if history state should be replaced instead of pushed). | 
 |    *     hash (a hash state to attach to the page). | 
 |    */ | 
 |   showPageByName(pageName, opt_updateHistory, opt_propertyBag) { | 
 |     opt_updateHistory = opt_updateHistory !== false; | 
 |     opt_propertyBag = opt_propertyBag || {}; | 
 |  | 
 |     // Find the currently visible root-level page. | 
 |     let rootPage = null; | 
 |     for (const page of this.registeredPages.values()) { | 
 |       if (page.visible && !page.parentPage) { | 
 |         rootPage = page; | 
 |         break; | 
 |       } | 
 |     } | 
 |  | 
 |     // Find the target page. | 
 |     let targetPage = this.registeredPages.get(pageName.toLowerCase()); | 
 |     if (!targetPage) { | 
 |       targetPage = this.defaultPage_; | 
 |     } | 
 |  | 
 |     pageName = targetPage.name.toLowerCase(); | 
 |     const targetPageWasVisible = targetPage.visible; | 
 |  | 
 |     // Notify pages if they will be hidden. | 
 |     this.registeredPages.forEach(page => { | 
 |       if (page.name != pageName && !this.isAncestorOfPage(page, targetPage)) { | 
 |         page.willHidePage(); | 
 |       } | 
 |     }); | 
 |  | 
 |     // Update the page's hash. | 
 |     targetPage.hash = opt_propertyBag.hash || ''; | 
 |  | 
 |     // Update visibilities to show only the hierarchy of the target page. | 
 |     this.registeredPages.forEach(page => { | 
 |       page.visible = | 
 |           page.name == pageName || this.isAncestorOfPage(page, targetPage); | 
 |     }); | 
 |  | 
 |     // Update the history and current location. | 
 |     if (opt_updateHistory) { | 
 |       this.updateHistoryState_(!!opt_propertyBag.replaceState); | 
 |     } | 
 |  | 
 |     // Update focus if any other control was focused on the previous page, | 
 |     // or the previous page is not known. | 
 |     if (document.activeElement != document.body && | 
 |         (!rootPage || rootPage.pageDiv.contains(document.activeElement))) { | 
 |       targetPage.focus(); | 
 |     } | 
 |  | 
 |     // Notify pages if they were shown. | 
 |     this.registeredPages.forEach(page => { | 
 |       if (!targetPageWasVisible && | 
 |           (page.name == pageName || this.isAncestorOfPage(page, targetPage))) { | 
 |         page.didShowPage(); | 
 |       } | 
 |     }); | 
 |  | 
 |     // If the target page was already visible, notify it that its hash | 
 |     // changed externally. | 
 |     if (targetPageWasVisible) { | 
 |       targetPage.didChangeHash(); | 
 |     } | 
 |  | 
 |     // Update the document title. Do this after didShowPage was called, in | 
 |     // case a page decides to change its title. | 
 |     this.updateTitle_(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Returns the name of the page from the current path. | 
 |    * @return {string} Name of the page specified by the current path. | 
 |    */ | 
 |   getPageNameFromPath() { | 
 |     const path = location.pathname; | 
 |     if (path.length <= 1) { | 
 |       return this.defaultPage_.name; | 
 |     } | 
 |  | 
 |     // Skip starting slash and remove trailing slash (if any). | 
 |     return path.slice(1).replace(/\/$/, ''); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Gets the level of the page. Root pages (e.g., BrowserOptions) are at | 
 |    * level 0. | 
 |    * @return {number} How far down this page is from the root page. | 
 |    */ | 
 |   getNestingLevel(page) { | 
 |     let level = 0; | 
 |     let parent = page.parentPage; | 
 |     while (parent) { | 
 |       level++; | 
 |       parent = parent.parentPage; | 
 |     } | 
 |     return level; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Checks whether one page is an ancestor of the other page in terms of | 
 |    * subpage nesting. | 
 |    * @param {Page} potentialAncestor Potential ancestor. | 
 |    * @param {Page} potentialDescendent Potential descendent. | 
 |    * @return {boolean} True if |potentialDescendent| is nested under | 
 |    *     |potentialAncestor|. | 
 |    */ | 
 |   isAncestorOfPage(potentialAncestor, potentialDescendent) { | 
 |     let parent = potentialDescendent.parentPage; | 
 |     while (parent) { | 
 |       if (parent == potentialAncestor) { | 
 |         return true; | 
 |       } | 
 |       parent = parent.parentPage; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Called when a page's hash changes. If the page is the topmost visible | 
 |    * page, the history state is updated. | 
 |    * @param {!CustomEvent} e | 
 |    */ | 
 |   onPageHashChanged_(e) { | 
 |     const page = /** @type {!Page} */ (e.target); | 
 |     if (page == this.getTopmostVisiblePage()) { | 
 |       this.updateHistoryState_(false); | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {!PageManagerObserver} observer The observer to register. | 
 |    */ | 
 |   addObserver(observer) { | 
 |     this.observers_.push(observer); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Returns the topmost visible page. | 
 |    * @return {Page} | 
 |    * @private | 
 |    */ | 
 |   getTopmostVisiblePage() { | 
 |     for (const page of this.registeredPages.values()) { | 
 |       if (page.visible) { | 
 |         return page; | 
 |       } | 
 |     } | 
 |  | 
 |     return null; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Updates the title to the title of the current page, or of the topmost | 
 |    * visible page with a non-empty title. | 
 |    * @private | 
 |    */ | 
 |   updateTitle_() { | 
 |     let page = this.getTopmostVisiblePage(); | 
 |     while (page) { | 
 |       if (page.title) { | 
 |         for (let i = 0; i < this.observers_.length; ++i) { | 
 |           this.observers_[i].updateTitle(page.title); | 
 |         } | 
 |         return; | 
 |       } | 
 |       page = page.parentPage; | 
 |     } | 
 |   } | 
 |  | 
 |   /** | 
 |    * Constructs a new path to push onto the history stack, using observers | 
 |    * to update the history. | 
 |    * @param {boolean} replace If true, handlers should replace the current | 
 |    *     history event rather than create new ones. | 
 |    * @private | 
 |    */ | 
 |   updateHistoryState_(replace) { | 
 |     if (this.isDialog) { | 
 |       return; | 
 |     } | 
 |  | 
 |     const page = this.getTopmostVisiblePage(); | 
 |     let path = window.location.pathname + window.location.hash; | 
 |     if (path) { | 
 |       // Remove trailing slash. | 
 |       path = path.slice(1).replace(/\/(?:#|$)/, ''); | 
 |     } | 
 |  | 
 |     // If the page is already in history (the user may have clicked the same | 
 |     // link twice, or this is the initial load), do nothing. | 
 |     const newPath = (page == this.defaultPage_ ? '' : page.name) + page.hash; | 
 |     if (path == newPath) { | 
 |       return; | 
 |     } | 
 |  | 
 |     for (let i = 0; i < this.observers_.length; ++i) { | 
 |       this.observers_[i].updateHistory(newPath, replace); | 
 |     } | 
 |   } | 
 | } | 
 |  | 
 | /** | 
 |  * An observer of PageManager. | 
 |  */ | 
 | export class PageManagerObserver { | 
 |   /** | 
 |    * Called when a new title should be set. | 
 |    * @param {string} title The title to set. | 
 |    */ | 
 |   updateTitle(title) {} | 
 |  | 
 |   /** | 
 |    * Called when a page is navigated to. | 
 |    * @param {string} path The path of the page being visited. | 
 |    * @param {boolean} replace If true, allow no history events to be created. | 
 |    */ | 
 |   updateHistory(path, replace) {} | 
 | } | 
 |  | 
 | addSingletonGetter(PageManager); |