chromium/chrome/browser/resources/bluetooth_internals/page.js

// 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;
  }
}