blob: ce691ec525f2e672ee77d1ea8000ab9bdedb9f3b [file] [log] [blame]
// Copyright 2014 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';
function getRequiredElement(id) {
// Disable getElementById restriction here, because this UI uses non valid
// selectors that don't work with querySelector().
// eslint-disable-next-line no-restricted-properties
const el = document.getElementById(id);
assert(el instanceof HTMLElement);
return el;
}
/**
* Finds a good place to set initial focus. Generally called when UI is shown.
* @param {!Element} root Where to start looking for focusable controls.
*/
function setInitialFocus(root) {
// Do not change focus if any element in |root| is already focused.
if (root.contains(document.activeElement)) {
return;
}
const elements =
root.querySelectorAll('input, list, select, textarea, button');
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.focus();
// .focus() isn't guaranteed to work. Continue until it does.
if (document.activeElement === element) {
return;
}
}
}
/**
* Base class for pages that can be shown and hidden by PageManager. Each Page
* is like a node in a forest, corresponding to a particular div. At any
* point, one root Page is visible, and any visible Page can show a child Page
* as an overlay. The host of the root Page(s) should provide a container div
* for each nested level to enforce the stack order of overlays.
*/
export class Page extends EventTarget {
/**
* @param {string} name Page name.
* @param {string} title Page title, used for history.
* @param {string} pageDivName ID of the div corresponding to the page.
*/
constructor(name, title, pageDivName) {
super();
this.name = name;
this.title = title;
this.pageDivName = pageDivName;
this.pageDiv = getRequiredElement(this.pageDivName);
// |pageDiv.page| is set to the page object (this) when the page is
// visible to track which page is being shown when multiple pages can
// share the same underlying div.
this.pageDiv.page = null;
this.tab = null;
this.lastFocusedElement = null;
this.hash = '';
/**
* The parent page of this page; or null for root pages.
* @type {Page}
*/
this.parentPage = null;
/**
* The section on the parent page that is associated with this page.
* Can be null.
* @type {Element}
*/
this.associatedSection = null;
/**
* An array of controls that are associated with this page. The first
* control should be located on a root page.
* @type {Array<Element>}
*/
this.associatedControls = null;
}
/**
* Initializes page content.
*/
initializePage() {}
/**
* Called by the PageManager when this.hash changes while the page is
* already visible. This is analogous to the hashchange DOM event.
*/
didChangeHash() {}
/**
* Sets focus on the first focusable element. Override for a custom focus
* strategy.
*/
focus() {
setInitialFocus(this.pageDiv);
}
/**
* Updates the hash of the current page. If the page is topmost, the history
* state is updated.
* @param {string} hash The new hash value. Like location.hash, this
* should include the leading '#' if not empty.
*/
setHash(hash) {
if (this.hash === hash) {
return;
}
this.hash = hash;
this.dispatchEvent(new CustomEvent('page-hash-changed'));
}
/**
* Called after the page has been shown.
*/
didShowPage() {}
/**
* Called before the page will be hidden, e.g., when a different root page
* will be shown.
*/
willHidePage() {}
/**
* Called after the overlay has been closed.
*/
didClosePage() {}
/**
* Gets page visibility state.
* @type {boolean}
*/
get visible() {
if (this.pageDiv.hidden) {
return false;
}
return this.pageDiv.page === this;
}
/**
* Sets page visibility.
* @type {boolean}
*/
set visible(visible) {
if ((this.visible && visible) || (!this.visible && !visible)) {
return;
}
this.pageDiv.page = this;
this.pageDiv.hidden = !visible;
}
}