chromium/tools/binary_size/libsupersize/viewer/static/dom.js

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview
 * Constants, utilities, and objects for DOM access.
 */

/** @enum {string} Keys in query string and names of input elements. */
const STATE_KEY = {
  LOAD_URL: 'load_url',
  BEFORE_URL: 'before_url',
  BYTE_UNIT: 'byteunit',
  METHOD_COUNT: 'method_count',
  MIN_SIZE: 'min_size',
  GROUP_BY: 'group_by',
  INCLUDE: 'include',
  EXCLUDE: 'exclude',
  TYPE: 'type',
  FLAG_FILTER: 'flag_filter',
  FOCUS: 'focus',
};

/** Utilities for working with the DOM */
const dom = {
  /**
   * Creates a document fragment from the given nodes.
   * @param {Iterable<Node>} nodes
   * @return {!DocumentFragment}
   */
  createFragment(nodes) {
    const fragment = document.createDocumentFragment();
    for (const node of nodes)
      fragment.appendChild(node);
    return fragment;
  },
  /**
   * Removes all the existing children of `parent` and inserts `newChild` in
   * their place.
   * @param {!Element} parent
   * @param {Node | null} newChild
   */
  replace(parent, newChild) {
    parent.innerHTML = '';
    if (newChild)
      parent.appendChild(newChild);
  },
  /**
   * Builds a text element in a single statement.
   * @param {string} tagName Type of the element, such as "span".
   * @param {string} text Text content for the element.
   * @param {string} [className] Class to apply to the element.
   */
  textElement(tagName, text, className) {
    const element = document.createElement(tagName);
    element.textContent = text;
    if (className)
      element.className = className;
    return element;
  },
  /**
   * Schedule a one-time |task| call on next animation frame when |node| is
   * added to the DOM, or if |node| is already in the DOM.
   * @param {!Node} node
   * @param {!function(): *} task
   */
  onNodeAdded(node, task) {
    if (document.contains(node)) {
      requestAnimationFrame(task);
      return;
    }
    let found = false;
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.contains(node)) {
            observer.disconnect();
            found = true;
            requestAnimationFrame(task);
            return;
          }
        }
      }
    });
    observer.observe(document, {subtree: true, childList: true});
  },
};

/** Centralized object for element access. */
class MainElements {
  constructor() {
    /** @public {!NodeList} Elements that toggle body.show-options on click. */
    this.nlShowOptions =
        /** @type {!NodeList} */ (document.querySelectorAll('.toggle-options'));

    /** @public {!HTMLDivElement} */
    this.divReviewInfo =
        /** @type {!HTMLDivElement} */ (this.query('#div-review-info'));

    /** @type {!HTMLAnchorElement} */
    this.linkReviewText =
        /** @type {!HTMLAnchorElement} */ (this.query('#link-review-text'));

    /** @type {!HTMLAnchorElement} */
    this.linkDownloadBefore =
        /** @type {!HTMLAnchorElement} */ (this.query('#link-download-before'));

    /** @type {!HTMLAnchorElement} */
    this.linkDownloadLoad =
        /** @type {!HTMLAnchorElement} */ (this.query('#link-download-load'));

    /** @type {!HTMLInputElement} */
    this.fileUpload =
        /** @type {!HTMLInputElement} */ (this.query('#file-upload'));

    /** @type {!HTMLAnchorElement} */
    this.linkFaq =
        /** @type {!HTMLAnchorElement} */ (this.query('#link-faq'));

    /** @type {!HTMLProgressElement} */
    this.progAppbar =
        /** @type {!HTMLProgressElement} */ (this.query('#prog-appbar'));

    /** @public {!HTMLFormElement} Form with options and filters. */
    this.frmOptions =
        /** @type {!HTMLFormElement} */ (this.query('#frm-options'));

    /** @public {!HTMLInputElement} */
    this.cbMethodCount =
        /** @type {!HTMLInputElement} */ (this.query('#cb-method-count'));

    /** @public {!HTMLSelectElement} */
    this.selByteUnit =
        /** @type {!HTMLSelectElement} */ (this.query('#sel-byte-unit'));

    /** @public {!HTMLInputElement} */
    this.nbMinSize =
        /** @type {!HTMLInputElement} */ (this.query('#nb-min-size'));

    /** @public {!RadioNodeList} */
    this.rnlGroupBy = /** @type {!RadioNodeList} */ (
        this.frmOptions.elements.namedItem(STATE_KEY.GROUP_BY));
    assert(this.rnlGroupBy.length > 0);

    /** @public {!HTMLInputElement} */
    this.tbIncludeRegex =
        /** @type {!HTMLInputElement} */ (this.query('#tb-include-regex'));

    /** @public {!HTMLInputElement} */
    this.tbExcludeRegex =
        /** @type {!HTMLInputElement} */ (this.query('#tb-exclude-regex'));

    /** @public {!RadioNodeList} */
    this.rnlType = /** @type {!RadioNodeList} */ (
        this.frmOptions.elements.namedItem(STATE_KEY.TYPE));
    assert(this.rnlType.length > 0);

    /** @type {!HTMLFieldSetElement} */
    this.fsTypesFilter =
        /** @type {!HTMLFieldSetElement} */ (this.query('#fs-types-filter'));

    /** @type {!HTMLButtonElement} */
    this.btnTypeAll =
        /** @type {!HTMLButtonElement} */ (this.query('#btn-type-all'));

    /** @type {!HTMLButtonElement} */
    this.btnTypeNone =
        /** @type {!HTMLButtonElement} */ (this.query('#btn-type-none'));

    /** @public {!RadioNodeList} */
    this.rnlFlagFilter = /** @type {!RadioNodeList} */ (
        this.frmOptions.elements.namedItem(STATE_KEY.FLAG_FILTER));
    assert(this.rnlFlagFilter.length > 0);

    /** @public {!HTMLDivElement} */
    this.divIcons =
        /** @type {!HTMLDivElement} */ (this.query('#div-icons'));

    /** @public {!HTMLDivElement} */
    this.divDiffStatusIcons =
        /** @type {!HTMLDivElement} */ (this.query('#div-diff-status-icons'));

    /** @public {!HTMLDivElement} */
    this.divMiscIcons =
        /** @type {!HTMLDivElement} */ (this.query('#div-misc-icons'));

    /** @type {!HTMLTemplateElement} Template for groups in the Symbol Tree. */
    this.tmplSymbolTreeGroup = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-symbol-tree-group'));

    /** @type {!HTMLTemplateElement} Template for leaves in the Symbol Tree. */
    this.tmplSymbolTreeLeaf = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-symbol-tree-leaf'));

    /** @type {!HTMLSpanElement} */
    this.spanSizeHeader =
        /** @type {!HTMLSpanElement} */ (this.query('#span-size-header'));

    /** @type {!HTMLUListElement} */
    this.ulSymbolTree =
        /** @type {!HTMLUListElement} */ (this.query('#ul-symbol-tree'));

    /** @type {!HTMLTemplateElement} Template for groups in the Metrics Tree. */
    this.tmplMetricsTreeGroup = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-metrics-tree-group'));

    /** @type {!HTMLTemplateElement} Template for leaves in the Metrics Tree. */
    this.tmplMetricsTreeLeaf = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-metrics-tree-leaf'));

    /** @public {!HTMLDivElement} */
    this.divMetricsView =
        /** @type {!HTMLDivElement} */ (this.query('#div-metrics-view'));

    /** @type {!HTMLUListElement} */
    this.ulMetricsTree =
        /** @type {!HTMLUListElement} */ (this.query('#ul-metrics-tree'));

    /** @public {!HTMLDivElement} */
    this.divNoSymbolsMsg =
        /** @type {!HTMLDivElement} */ (this.query('#div-no-symbols-msg'));

    /**
     * @type {!HTMLTemplateElement} Template for groups in the Metadata Tree.
     */
    this.tmplMetadataTreeGroup = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-metadata-tree-group'));

    /**
     * @type {!HTMLTemplateElement} Template for leaves in the Metadata Tree.
     */
    this.tmplMetadataTreeLeaf = /** @type {!HTMLTemplateElement} */ (
        this.query('#tmpl-metadata-tree-leaf'));

    /** @public {!HTMLDivElement} */
    this.divMetadataView =
        /** @type {!HTMLDivElement} */ (this.query('#div-metadata-view'));

    /** @type {!HTMLUListElement} */
    this.ulMetadataTree =
        /** @type {!HTMLUListElement} */ (this.query('#ul-metadata-tree'));

    /** @public {!HTMLDivElement} */
    this.divInfocardArtifact =
        /** @type {!HTMLDivElement} */ (this.query('#div-infocard-artifact'));

    /** @public {!HTMLDivElement} */
    this.divInfocardSymbol =
        /** @type {!HTMLDivElement} */ (this.query('#div-infocard-symbol'));

    /** @public {!HTMLDivElement} */
    this.divSigninModal =
        /** @type {!HTMLDivElement} */ (this.query('#div-signin-modal'));

    /** @public {!HTMLDivElement} */
    this.divDisassemblyModal =
        /** @type {!HTMLDivElement} */ (this.query('#div-disassembly-modal'));
  }

  /**
   * @param {string} q Query string.
   * @return {!Element}
   * @private
   */
  query(q) {
    return /** @type {!Element} */ (assertNotNull(document.querySelector(q)));
  }

  /**
   * @param {!Element} elt
   * @return {!Element}
   * @public
   */
  getAriaDescribedBy(elt) {
    const id = assertNotNull(elt.getAttribute('aria-describedby'));
    return assertNotNull(document.getElementById(id));
  }
}

/** @const {!MainElements} */
const g_el = new MainElements();