tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

computed.js (54642B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const flags = require("resource://devtools/shared/flags.js");
      8 const ToolDefinitions =
      9  require("resource://devtools/client/definitions.js").Tools;
     10 const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
     11 const {
     12  style: { ELEMENT_STYLE, PRES_HINTS },
     13 } = require("resource://devtools/shared/constants.js");
     14 const OutputParser = require("resource://devtools/client/shared/output-parser.js");
     15 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
     16 const {
     17  createChild,
     18 } = require("resource://devtools/client/inspector/shared/utils.js");
     19 const {
     20  VIEW_NODE_SELECTOR_TYPE,
     21  VIEW_NODE_PROPERTY_TYPE,
     22  VIEW_NODE_VALUE_TYPE,
     23  VIEW_NODE_IMAGE_URL_TYPE,
     24  VIEW_NODE_FONT_TYPE,
     25 } = require("resource://devtools/client/inspector/shared/node-types.js");
     26 const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js");
     27 
     28 loader.lazyRequireGetter(
     29  this,
     30  "StyleInspectorMenu",
     31  "resource://devtools/client/inspector/shared/style-inspector-menu.js"
     32 );
     33 loader.lazyRequireGetter(
     34  this,
     35  "KeyShortcuts",
     36  "resource://devtools/client/shared/key-shortcuts.js"
     37 );
     38 loader.lazyRequireGetter(
     39  this,
     40  "clipboardHelper",
     41  "resource://devtools/shared/platform/clipboard.js"
     42 );
     43 loader.lazyRequireGetter(
     44  this,
     45  "openContentLink",
     46  "resource://devtools/client/shared/link.js",
     47  true
     48 );
     49 const lazy = {};
     50 ChromeUtils.defineESModuleGetters(lazy, {
     51  getMdnLinkParams: "resource://devtools/shared/mdn.mjs",
     52 });
     53 
     54 const STYLE_INSPECTOR_PROPERTIES =
     55  "devtools/shared/locales/styleinspector.properties";
     56 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     57 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
     58 const L10N_TWISTY_EXPAND_LABEL = STYLE_INSPECTOR_L10N.getStr(
     59  "rule.twistyExpand.label"
     60 );
     61 const L10N_TWISTY_COLLAPSE_LABEL = STYLE_INSPECTOR_L10N.getStr(
     62  "rule.twistyCollapse.label"
     63 );
     64 const L10N_EMPTY_VARIABLE = STYLE_INSPECTOR_L10N.getStr("rule.variableEmpty");
     65 
     66 const FILTER_CHANGED_TIMEOUT = 150;
     67 
     68 /**
     69 * Helper for long-running processes that should yield occasionally to
     70 * the mainloop.
     71 */
     72 class UpdateProcess {
     73  /**
     74   * @param {Window} win
     75   *        Timeouts will be set on this window when appropriate.
     76   * @param {Array} array
     77   *        The array of items to process.
     78   * @param {object} options
     79   *        Options for the update process:
     80   *          onItem {function} Will be called with the value of each iteration.
     81   *          onBatch {function} Will be called after each batch of iterations,
     82   *            before yielding to the main loop.
     83   *          onDone {function} Will be called when iteration is complete.
     84   *          onCancel {function} Will be called if the process is canceled.
     85   *          threshold {int} How long to process before yielding, in ms.
     86   */
     87  constructor(win, array, options) {
     88    this.win = win;
     89    this.index = 0;
     90    this.array = array;
     91 
     92    this.onItem = options.onItem || function () {};
     93    this.onBatch = options.onBatch || function () {};
     94    this.onDone = options.onDone || function () {};
     95    this.onCancel = options.onCancel || function () {};
     96    this.threshold = options.threshold || 45;
     97  }
     98 
     99  #canceled = false;
    100  #timeout = null;
    101 
    102  /**
    103   * Symbol returned when the array of items to process is empty.
    104   */
    105  static ITERATION_DONE = Symbol("UpdateProcess iteration done");
    106 
    107  /**
    108   * Schedule a new batch on the main loop.
    109   */
    110  schedule() {
    111    if (this.#canceled) {
    112      return;
    113    }
    114    this.#timeout = setTimeout(() => this.#timeoutHandler(), 0);
    115  }
    116 
    117  /**
    118   * Cancel the running process.  onItem will not be called again,
    119   * and onCancel will be called.
    120   */
    121  cancel() {
    122    if (this.#timeout) {
    123      clearTimeout(this.#timeout);
    124      this.#timeout = null;
    125    }
    126    this.#canceled = true;
    127    this.onCancel();
    128  }
    129 
    130  #timeoutHandler() {
    131    this.#timeout = null;
    132    if (this.#runBatch() === UpdateProcess.ITERATION_DONE) {
    133      this.onBatch();
    134      this.onDone();
    135      return;
    136    }
    137    this.schedule();
    138  }
    139 
    140  #runBatch() {
    141    const time = Date.now();
    142    while (!this.#canceled) {
    143      const next = this.#next();
    144      if (next === UpdateProcess.ITERATION_DONE) {
    145        return next;
    146      }
    147 
    148      this.onItem(next);
    149      if (Date.now() - time > this.threshold) {
    150        this.onBatch();
    151        return null;
    152      }
    153    }
    154    return null;
    155  }
    156 
    157  /**
    158   * Returns the item at the current index and increases the index.
    159   * If all items have already been processed, will return ITERATION_DONE.
    160   */
    161  #next() {
    162    if (this.index < this.array.length) {
    163      return this.array[this.index++];
    164    }
    165    return UpdateProcess.ITERATION_DONE;
    166  }
    167 }
    168 
    169 /**
    170 * CssComputedView is a panel that manages the display of a table
    171 * sorted by style. There should be one instance of CssComputedView
    172 * per style display (of which there will generally only be one).
    173 */
    174 class CssComputedView {
    175  /**
    176   * @param {Inspector} inspector
    177   *        Inspector toolbox panel
    178   * @param {Document} document
    179   *        The document that will contain the computed view.
    180   */
    181  constructor(inspector, document) {
    182    this.inspector = inspector;
    183    this.styleDocument = document;
    184    this.styleWindow = this.styleDocument.defaultView;
    185 
    186    this.propertyViews = [];
    187 
    188    this.#outputParser = new OutputParser(document, inspector.cssProperties);
    189 
    190    // Create bound methods.
    191    this.focusWindow = this.focusWindow.bind(this);
    192    this.refreshPanel = this.refreshPanel.bind(this);
    193 
    194    const doc = this.styleDocument;
    195    this.element = doc.getElementById("computed-property-container");
    196    this.searchField = doc.getElementById("computed-searchbox");
    197    this.searchClearButton = doc.getElementById("computed-searchinput-clear");
    198    this.includeBrowserStylesCheckbox = doc.getElementById(
    199      "browser-style-checkbox"
    200    );
    201 
    202    this.#abortController = new AbortController();
    203    const opts = { signal: this.#abortController.signal };
    204 
    205    this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
    206    this.shortcuts.on(
    207      "CmdOrCtrl+F",
    208      event => this.#onShortcut("CmdOrCtrl+F", event),
    209      opts
    210    );
    211    this.shortcuts.on(
    212      "Escape",
    213      event => this.#onShortcut("Escape", event),
    214      opts
    215    );
    216    this.styleDocument.addEventListener("copy", this.#onCopy, opts);
    217    this.styleDocument.addEventListener("mousedown", this.focusWindow, opts);
    218    this.element.addEventListener("click", this.#onClick, opts);
    219    this.element.addEventListener("contextmenu", this.#onContextMenu, opts);
    220    this.searchField.addEventListener("input", this.#onFilterStyles, opts);
    221    this.searchClearButton.addEventListener("click", this.#onClearSearch, opts);
    222    this.includeBrowserStylesCheckbox.addEventListener(
    223      "input",
    224      this.#onIncludeBrowserStyles,
    225      opts
    226    );
    227 
    228    if (flags.testing) {
    229      // In tests, we start listening immediately to avoid having to simulate a mousemove.
    230      this.highlighters.addToView(this);
    231    } else {
    232      this.element.addEventListener(
    233        "mousemove",
    234        () => {
    235          this.highlighters.addToView(this);
    236        },
    237        { once: true, signal: this.#abortController.signal }
    238      );
    239    }
    240 
    241    if (!this.inspector.isThreePaneModeEnabled) {
    242      // When the rules view is added in 3 pane mode, refresh the Computed view whenever
    243      // the rules are changed.
    244      this.inspector.once(
    245        "ruleview-added",
    246        () => {
    247          this.ruleView.on("ruleview-changed", this.refreshPanel, opts);
    248        },
    249        opts
    250      );
    251    }
    252 
    253    if (this.ruleView) {
    254      this.ruleView.on("ruleview-changed", this.refreshPanel, opts);
    255    }
    256 
    257    this.searchClearButton.hidden = true;
    258 
    259    // No results text.
    260    this.noResults = this.styleDocument.getElementById("computed-no-results");
    261 
    262    // Refresh panel when color unit changed or pref for showing
    263    // original sources changes.
    264    this.#prefObserver = new PrefObserver("devtools.");
    265    this.#prefObserver.on(
    266      "devtools.defaultColorUnit",
    267      this.#handlePrefChange,
    268      opts
    269    );
    270 
    271    // The PageStyle front related to the currently selected element
    272    this.viewedElementPageStyle = null;
    273    // Flag that is set when the selected element style was updated. This will force
    274    // clearing the page style cssLogic cache the next time we're calling getComputed().
    275    this.elementStyleUpdated = false;
    276 
    277    this.createStyleViews();
    278 
    279    // Add the tooltips and highlightersoverlay
    280    this.tooltips = new TooltipsOverlay(this);
    281  }
    282 
    283  /**
    284   * Lookup a l10n string in the shared styleinspector string bundle.
    285   *
    286   * @param {string} name
    287   *        The key to lookup.
    288   * @returns {string} localized version of the given key.
    289   */
    290  static l10n(name) {
    291    try {
    292      return STYLE_INSPECTOR_L10N.getStr(name);
    293    } catch (ex) {
    294      console.log("Error reading '" + name + "'");
    295      throw new Error("l10n error with " + name);
    296    }
    297  }
    298 
    299  #abortController;
    300  #contextMenu;
    301  #computed;
    302  #createViewsProcess;
    303  #createViewsPromise;
    304  // Used for cancelling timeouts in the style filter.
    305  #filterChangedTimeout = null;
    306  #highlighters;
    307  #isDestroyed = false;
    308  // Cache the list of properties that match the selected element.
    309  #matchedProperties = null;
    310  #outputParser = null;
    311  #prefObserver;
    312  #refreshProcess;
    313  #sourceFilter;
    314  // The element that we're inspecting, and the document that it comes from.
    315  #viewedElement = null;
    316 
    317  // Number of visible properties
    318  numVisibleProperties = 0;
    319 
    320  get outputParser() {
    321    return this.#outputParser;
    322  }
    323 
    324  get computed() {
    325    return this.#computed;
    326  }
    327 
    328  get contextMenu() {
    329    if (!this.#contextMenu) {
    330      this.#contextMenu = new StyleInspectorMenu(this);
    331    }
    332 
    333    return this.#contextMenu;
    334  }
    335 
    336  // Get the highlighters overlay from the Inspector.
    337  get highlighters() {
    338    if (!this.#highlighters) {
    339      // highlighters is a lazy getter in the inspector.
    340      this.#highlighters = this.inspector.highlighters;
    341    }
    342 
    343    return this.#highlighters;
    344  }
    345 
    346  get includeBrowserStyles() {
    347    return this.includeBrowserStylesCheckbox.checked;
    348  }
    349 
    350  get ruleView() {
    351    return (
    352      this.inspector.hasPanel("ruleview") &&
    353      this.inspector.getPanel("ruleview").view
    354    );
    355  }
    356 
    357  get viewedElement() {
    358    return this.#viewedElement;
    359  }
    360 
    361  #handlePrefChange = () => {
    362    if (this.#computed) {
    363      this.refreshPanel();
    364    }
    365  };
    366 
    367  /**
    368   * Update the view with a new selected element. The CssComputedView panel
    369   * will show the style information for the given element.
    370   *
    371   * @param {NodeFront} element
    372   *        The highlighted node to get styles for.
    373   * @returns a promise that will be resolved when highlighting is complete.
    374   */
    375  selectElement(element) {
    376    if (!element) {
    377      if (this.viewedElementPageStyle) {
    378        this.viewedElementPageStyle.off(
    379          "stylesheet-updated",
    380          this.refreshPanel
    381        );
    382        this.viewedElementPageStyle = null;
    383      }
    384      this.#viewedElement = null;
    385      this.noResults.hidden = false;
    386 
    387      if (this.#refreshProcess) {
    388        this.#refreshProcess.cancel();
    389      }
    390      // Hiding all properties
    391      for (const propView of this.propertyViews) {
    392        propView.refresh();
    393      }
    394      return Promise.resolve(undefined);
    395    }
    396 
    397    if (element === this.#viewedElement) {
    398      return Promise.resolve(undefined);
    399    }
    400 
    401    if (this.viewedElementPageStyle) {
    402      this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel);
    403    }
    404    this.viewedElementPageStyle = element.inspectorFront.pageStyle;
    405    this.viewedElementPageStyle.on("stylesheet-updated", this.refreshPanel, {
    406      signal: this.#abortController.signal,
    407    });
    408 
    409    this.#viewedElement = element;
    410 
    411    this.refreshSourceFilter();
    412 
    413    return this.refreshPanel();
    414  }
    415 
    416  /**
    417   * Get the type of a given node in the computed-view
    418   *
    419   * @param {DOMNode} node
    420   *        The node which we want information about
    421   * @return {object} The type information object contains the following props:
    422   * - view {String} Always "computed" to indicate the computed view.
    423   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
    424   *   client/inspector/shared/node-types
    425   * - value {Object} Depends on the type of the node
    426   * returns null if the node isn't anything we care about
    427   */
    428  // eslint-disable-next-line complexity
    429  getNodeInfo(node) {
    430    if (!node) {
    431      return null;
    432    }
    433 
    434    const classes = node.classList;
    435 
    436    // Check if the node isn't a selector first since this doesn't require
    437    // walking the DOM
    438    if (
    439      classes.contains("matched") ||
    440      classes.contains("bestmatch") ||
    441      classes.contains("parentmatch")
    442    ) {
    443      let selectorText = "";
    444 
    445      for (const child of node.childNodes[1].childNodes) {
    446        if (child.nodeType === node.TEXT_NODE) {
    447          selectorText += child.textContent;
    448        }
    449      }
    450      return {
    451        type: VIEW_NODE_SELECTOR_TYPE,
    452        value: selectorText.trim(),
    453      };
    454    }
    455 
    456    const propertyView = node.closest(".computed-property-view");
    457    const propertyMatchedSelectors = node.closest(".matchedselectors");
    458    const parent = propertyMatchedSelectors || propertyView;
    459 
    460    if (!parent) {
    461      return null;
    462    }
    463 
    464    let value, type;
    465 
    466    // Get the property and value for a node that's a property name or value
    467    const isHref =
    468      classes.contains("theme-link") && !classes.contains("computed-link");
    469 
    470    if (classes.contains("computed-font-family")) {
    471      if (propertyMatchedSelectors) {
    472        const view = propertyMatchedSelectors.closest("li");
    473        value = {
    474          property: view.querySelector(".computed-property-name").firstChild
    475            .textContent,
    476          value: node.parentNode.textContent,
    477        };
    478      } else if (propertyView) {
    479        value = {
    480          property: parent.querySelector(".computed-property-name").firstChild
    481            .textContent,
    482          value: node.parentNode.textContent,
    483        };
    484      } else {
    485        return null;
    486      }
    487    } else if (
    488      propertyMatchedSelectors &&
    489      (classes.contains("computed-other-property-value") || isHref)
    490    ) {
    491      const view = propertyMatchedSelectors.closest("li");
    492      value = {
    493        property: view.querySelector(".computed-property-name").firstChild
    494          .textContent,
    495        value: node.textContent,
    496      };
    497    } else if (
    498      propertyView &&
    499      (classes.contains("computed-property-name") ||
    500        classes.contains("computed-property-value") ||
    501        isHref)
    502    ) {
    503      value = {
    504        property: parent.querySelector(".computed-property-name").firstChild
    505          .textContent,
    506        value: parent.querySelector(".computed-property-value").textContent,
    507      };
    508    }
    509 
    510    // Get the type
    511    if (classes.contains("computed-property-name")) {
    512      type = VIEW_NODE_PROPERTY_TYPE;
    513    } else if (
    514      classes.contains("computed-property-value") ||
    515      classes.contains("computed-other-property-value")
    516    ) {
    517      type = VIEW_NODE_VALUE_TYPE;
    518    } else if (classes.contains("computed-font-family")) {
    519      type = VIEW_NODE_FONT_TYPE;
    520    } else if (isHref) {
    521      type = VIEW_NODE_IMAGE_URL_TYPE;
    522      value.url = node.href;
    523    } else {
    524      return null;
    525    }
    526 
    527    return {
    528      view: "computed",
    529      type,
    530      value,
    531    };
    532  }
    533 
    534  #createPropertyViews() {
    535    if (this.#createViewsPromise) {
    536      return this.#createViewsPromise;
    537    }
    538 
    539    this.refreshSourceFilter();
    540    this.numVisibleProperties = 0;
    541    const fragment = this.styleDocument.createDocumentFragment();
    542 
    543    this.#createViewsPromise = new Promise((resolve, reject) => {
    544      this.#createViewsProcess = new UpdateProcess(
    545        this.styleWindow,
    546        CssComputedView.propertyNames,
    547        {
    548          onItem: propertyName => {
    549            // Per-item callback.
    550            const propView = new PropertyView(this, propertyName);
    551            fragment.append(propView.createListItemElement());
    552 
    553            if (propView.visible) {
    554              this.numVisibleProperties++;
    555            }
    556            this.propertyViews.push(propView);
    557          },
    558          onCancel: () => {
    559            reject("#createPropertyViews cancelled");
    560          },
    561          onDone: () => {
    562            // Completed callback.
    563            this.element.appendChild(fragment);
    564            this.noResults.hidden = this.numVisibleProperties > 0;
    565            resolve(undefined);
    566          },
    567        }
    568      );
    569    });
    570 
    571    this.#createViewsProcess.schedule();
    572 
    573    return this.#createViewsPromise;
    574  }
    575 
    576  isPanelVisible() {
    577    return (
    578      this.inspector.toolbox &&
    579      this.inspector.sidebar &&
    580      this.inspector.toolbox.currentToolId === "inspector" &&
    581      this.inspector.sidebar.getCurrentTabID() == "computedview"
    582    );
    583  }
    584 
    585  /**
    586   * Refresh the panel content. This could be called by a "ruleview-changed" event, but
    587   * we avoid the extra processing unless the panel is visible.
    588   */
    589  async refreshPanel() {
    590    if (!this.#viewedElement || !this.isPanelVisible()) {
    591      return;
    592    }
    593 
    594    // Capture the current viewed element to return from the promise handler
    595    // early if it changed
    596    const viewedElement = this.#viewedElement;
    597 
    598    try {
    599      // Create the properties views only once for the whole lifecycle of the inspector
    600      // via `_createPropertyViews`.
    601      // The properties are created without backend data. This queries typical property
    602      // names via `DOMWindow.getComputedStyle` on the frontend inspector document.
    603      // We then have to manually update the list of PropertyView's for custom properties
    604      // based on backend data (`getComputed()`/`computed`).
    605      // Also note that PropertyView/PropertyView are refreshed via their refresh method
    606      // which will ultimately query `CssComputedView._computed`, which we update in this method.
    607      const [computed] = await Promise.all([
    608        this.viewedElementPageStyle.getComputed(this.#viewedElement, {
    609          filter: this.#sourceFilter,
    610          onlyMatched: !this.includeBrowserStyles,
    611          markMatched: true,
    612          clearCache: !!this.elementStyleUpdated,
    613        }),
    614        this.#createPropertyViews(),
    615      ]);
    616 
    617      this.elementStyleUpdated = false;
    618 
    619      if (viewedElement !== this.#viewedElement) {
    620        return;
    621      }
    622 
    623      this.#computed = computed;
    624      this.#matchedProperties = new Set();
    625      const customProperties = new Set();
    626 
    627      for (const name in computed) {
    628        if (computed[name].matched) {
    629          this.#matchedProperties.add(name);
    630        }
    631        if (name.startsWith("--")) {
    632          customProperties.add(name);
    633        }
    634      }
    635 
    636      // Removing custom property PropertyViews which won't be used
    637      let customPropertiesStartIndex;
    638      for (let i = this.propertyViews.length - 1; i >= 0; i--) {
    639        const propView = this.propertyViews[i];
    640 
    641        // custom properties are displayed at the bottom of the list, and we're looping
    642        // backward through propertyViews, so if the current item does not represent
    643        // a custom property, we can stop looping.
    644        if (!propView.isCustomProperty) {
    645          customPropertiesStartIndex = i + 1;
    646          break;
    647        }
    648 
    649        // If the custom property will be used, move to the next item.
    650        if (customProperties.has(propView.name)) {
    651          customProperties.delete(propView.name);
    652          continue;
    653        }
    654 
    655        // Otherwise remove property view element
    656        if (propView.element) {
    657          propView.element.remove();
    658        }
    659 
    660        propView.destroy();
    661        this.propertyViews.splice(i, 1);
    662      }
    663 
    664      // At this point, `customProperties` only contains custom property names for
    665      // which we don't have a PropertyView yet.
    666      let insertIndex = customPropertiesStartIndex;
    667      for (const customPropertyName of Array.from(customProperties).sort()) {
    668        const propertyView = new PropertyView(
    669          this,
    670          customPropertyName,
    671          // isCustomProperty
    672          true
    673        );
    674 
    675        const len = this.propertyViews.length;
    676        if (insertIndex !== len) {
    677          for (let i = insertIndex; i <= len; i++) {
    678            const existingPropView = this.propertyViews[i];
    679            if (
    680              !existingPropView ||
    681              !existingPropView.isCustomProperty ||
    682              customPropertyName < existingPropView.name
    683            ) {
    684              insertIndex = i;
    685              break;
    686            }
    687          }
    688        }
    689        this.propertyViews.splice(insertIndex, 0, propertyView);
    690 
    691        // Insert the custom property PropertyView at the right spot so we
    692        // keep the list ordered.
    693        const previousSibling = this.element.childNodes[insertIndex - 1];
    694        previousSibling.insertAdjacentElement(
    695          "afterend",
    696          propertyView.createListItemElement()
    697        );
    698      }
    699 
    700      if (this.#refreshProcess) {
    701        this.#refreshProcess.cancel();
    702      }
    703 
    704      this.noResults.hidden = true;
    705 
    706      // Reset visible property count
    707      this.numVisibleProperties = 0;
    708 
    709      await new Promise((resolve, reject) => {
    710        this.#refreshProcess = new UpdateProcess(
    711          this.styleWindow,
    712          this.propertyViews,
    713          {
    714            onItem: propView => {
    715              propView.refresh();
    716            },
    717            onCancel: () => {
    718              reject("#refreshProcess of computed view cancelled");
    719            },
    720            onDone: () => {
    721              this.#refreshProcess = null;
    722              this.noResults.hidden = this.numVisibleProperties > 0;
    723 
    724              const searchBox = this.searchField.parentNode;
    725              searchBox.classList.toggle(
    726                "devtools-searchbox-no-match",
    727                !!this.searchField.value.length && !this.numVisibleProperties
    728              );
    729 
    730              this.inspector.emit("computed-view-refreshed");
    731              resolve(undefined);
    732            },
    733          }
    734        );
    735        this.#refreshProcess.schedule();
    736      });
    737    } catch (e) {
    738      console.error(e);
    739    }
    740  }
    741 
    742  /**
    743   * Handle the shortcut events in the computed view.
    744   */
    745  #onShortcut = (name, event) => {
    746    if (!event.target.closest("#sidebar-panel-computedview")) {
    747      return;
    748    }
    749    // Handle the search box's keypress event. If the escape key is pressed,
    750    // clear the search box field.
    751    if (
    752      name === "Escape" &&
    753      event.target === this.searchField &&
    754      this.#onClearSearch()
    755    ) {
    756      event.preventDefault();
    757      event.stopPropagation();
    758    } else if (name === "CmdOrCtrl+F") {
    759      this.searchField.focus();
    760      event.preventDefault();
    761    }
    762  };
    763 
    764  /**
    765   * Set the filter style search value.
    766   *
    767   * @param {string} value
    768   *        The search value.
    769   */
    770  setFilterStyles(value = "") {
    771    this.searchField.value = value;
    772    this.searchField.focus();
    773    this.#onFilterStyles();
    774  }
    775 
    776  /**
    777   * Called when the user enters a search term in the filter style search box.
    778   */
    779  #onFilterStyles = () => {
    780    if (this.#filterChangedTimeout) {
    781      clearTimeout(this.#filterChangedTimeout);
    782    }
    783 
    784    const filterTimeout = this.searchField.value.length
    785      ? FILTER_CHANGED_TIMEOUT
    786      : 0;
    787    this.searchClearButton.hidden = this.searchField.value.length === 0;
    788 
    789    this.#filterChangedTimeout = setTimeout(() => {
    790      this.refreshPanel();
    791      this.#filterChangedTimeout = null;
    792    }, filterTimeout);
    793  };
    794 
    795  /**
    796   * Called when the user clicks on the clear button in the filter style search
    797   * box. Returns true if the search box is cleared and false otherwise.
    798   */
    799  #onClearSearch = () => {
    800    if (this.searchField.value) {
    801      this.setFilterStyles("");
    802      return true;
    803    }
    804 
    805    return false;
    806  };
    807 
    808  /**
    809   * The change event handler for the includeBrowserStyles checkbox.
    810   */
    811  #onIncludeBrowserStyles = () => {
    812    this.refreshSourceFilter();
    813    this.refreshPanel();
    814  };
    815 
    816  /**
    817   * When includeBrowserStylesCheckbox.checked is false we only display
    818   * properties that have matched selectors and have been included by the
    819   * document or one of thedocument's stylesheets. If .checked is false we
    820   * display all properties including those that come from UA stylesheets.
    821   */
    822  refreshSourceFilter() {
    823    this.#matchedProperties = null;
    824    this.#sourceFilter = this.includeBrowserStyles
    825      ? CssLogic.FILTER.UA
    826      : CssLogic.FILTER.USER;
    827  }
    828 
    829  /**
    830   * The CSS as displayed by the UI.
    831   */
    832  createStyleViews() {
    833    if (CssComputedView.propertyNames) {
    834      return;
    835    }
    836 
    837    CssComputedView.propertyNames = [];
    838 
    839    // Here we build and cache a list of css properties supported by the browser
    840    // We could use any element but let's use the main document's root element
    841    const styles = this.styleWindow.getComputedStyle(
    842      this.styleDocument.documentElement
    843    );
    844    const mozProps = [];
    845    for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
    846      const prop = styles.item(i);
    847      if (prop.startsWith("--")) {
    848        // Skip any CSS variables used inside of browser CSS files
    849        continue;
    850      } else if (prop.startsWith("-")) {
    851        mozProps.push(prop);
    852      } else {
    853        CssComputedView.propertyNames.push(prop);
    854      }
    855    }
    856 
    857    CssComputedView.propertyNames.sort();
    858    CssComputedView.propertyNames.push.apply(
    859      CssComputedView.propertyNames,
    860      mozProps.sort()
    861    );
    862 
    863    this.#createPropertyViews().catch(e => {
    864      if (!this.#isDestroyed) {
    865        console.warn(
    866          "The creation of property views was cancelled because " +
    867            "the computed-view was destroyed before it was done creating views"
    868        );
    869      } else {
    870        console.error(e);
    871      }
    872    });
    873  }
    874 
    875  /**
    876   * Get a set of properties that have matched selectors.
    877   *
    878   * @return {Set} If a property name is in the set, it has matching selectors.
    879   */
    880  get matchedProperties() {
    881    return this.#matchedProperties || new Set();
    882  }
    883 
    884  /**
    885   * Focus the window on mousedown.
    886   */
    887  focusWindow() {
    888    this.styleWindow.focus();
    889  }
    890 
    891  /**
    892   * Context menu handler.
    893   */
    894  #onContextMenu = event => {
    895    // Call stopPropagation() and preventDefault() here so that avoid to show default
    896    // context menu in about:devtools-toolbox. See Bug 1515265.
    897    event.stopPropagation();
    898    event.preventDefault();
    899    this.contextMenu.show(event);
    900  };
    901 
    902  #onClick = event => {
    903    const target = event.target;
    904 
    905    if (target.nodeName === "a") {
    906      event.stopPropagation();
    907      event.preventDefault();
    908      openContentLink(target.href);
    909    }
    910  };
    911 
    912  /**
    913   * Callback for copy event. Copy selected text.
    914   *
    915   * @param {Event} event
    916   *        copy event object.
    917   */
    918  #onCopy = event => {
    919    const win = this.styleWindow;
    920    const text = win.getSelection().toString().trim();
    921    if (text !== "") {
    922      this.copySelection();
    923      event.preventDefault();
    924    }
    925  };
    926 
    927  /**
    928   * Copy the current selection to the clipboard
    929   */
    930  copySelection() {
    931    try {
    932      const win = this.styleWindow;
    933      const text = win.getSelection().toString().trim();
    934 
    935      clipboardHelper.copyString(text);
    936    } catch (e) {
    937      console.error(e);
    938    }
    939  }
    940 
    941  /**
    942   * Destructor for CssComputedView.
    943   */
    944  destroy() {
    945    this.#viewedElement = null;
    946    this.#abortController.abort();
    947    this.#abortController = null;
    948 
    949    if (this.viewedElementPageStyle) {
    950      this.viewedElementPageStyle = null;
    951    }
    952    this.#outputParser = null;
    953 
    954    this.#prefObserver.destroy();
    955 
    956    // Cancel tree construction
    957    if (this.#createViewsProcess) {
    958      this.#createViewsProcess.cancel();
    959    }
    960    if (this.#refreshProcess) {
    961      this.#refreshProcess.cancel();
    962    }
    963 
    964    if (this.#contextMenu) {
    965      this.#contextMenu.destroy();
    966      this.#contextMenu = null;
    967    }
    968 
    969    if (this.#highlighters) {
    970      this.#highlighters.removeFromView(this);
    971      this.#highlighters = null;
    972    }
    973 
    974    this.tooltips.destroy();
    975 
    976    // Nodes used in templating
    977    this.element = null;
    978    this.searchField = null;
    979    this.searchClearButton = null;
    980    this.includeBrowserStylesCheckbox = null;
    981 
    982    // Property views
    983    for (const propView of this.propertyViews) {
    984      propView.destroy();
    985    }
    986    this.propertyViews = null;
    987 
    988    this.inspector = null;
    989    this.styleDocument = null;
    990    this.styleWindow = null;
    991 
    992    this.#isDestroyed = true;
    993  }
    994 }
    995 
    996 class PropertyInfo {
    997  /**
    998   * @param {CssComputedView} tree
    999   *        The CssComputedView instance we are working with.
   1000   * @param {string} name
   1001   *        The CSS property name
   1002   */
   1003  constructor(tree, name) {
   1004    this.#tree = tree;
   1005    this.name = name;
   1006  }
   1007 
   1008  #tree;
   1009 
   1010  get isSupported() {
   1011    // There can be a mismatch between the list of properties
   1012    // supported on the server and on the client.
   1013    // Ideally we should build PropertyInfo only for property names supported on
   1014    // the server. See Bug 1722348.
   1015    return this.#tree.computed && this.name in this.#tree.computed;
   1016  }
   1017 
   1018  get value() {
   1019    if (this.isSupported) {
   1020      const value = this.#tree.computed[this.name].value;
   1021      return value;
   1022    }
   1023    return null;
   1024  }
   1025 }
   1026 
   1027 /**
   1028 * A container to give easy access to property data from the template engine.
   1029 */
   1030 class PropertyView {
   1031  /**
   1032   * @param {CssComputedView} tree
   1033   *        The CssComputedView instance we are working with.
   1034   * @param {string} name
   1035   *        The CSS property name for which this PropertyView
   1036   *        instance will render the rules.
   1037   * @param {boolean} isCustomProperty
   1038   *        Set to true if this will represent a custom property.
   1039   */
   1040  constructor(tree, name, isCustomProperty = false) {
   1041    this.#tree = tree;
   1042    this.name = name;
   1043 
   1044    this.isCustomProperty = isCustomProperty;
   1045 
   1046    if (!this.isCustomProperty) {
   1047      this.link = `https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/${name}?${lazy.getMdnLinkParams("computed-panel")}`;
   1048    }
   1049 
   1050    this.#propertyInfo = new PropertyInfo(tree, name);
   1051    const win = this.#tree.styleWindow;
   1052    this.#abortController = new win.AbortController();
   1053  }
   1054 
   1055  // The parent element which contains the open attribute
   1056  element = null;
   1057 
   1058  // Destination for property values
   1059  valueNode = null;
   1060 
   1061  // Are matched rules expanded?
   1062  matchedExpanded = false;
   1063 
   1064  // Matched selector container
   1065  matchedSelectorsContainer = null;
   1066 
   1067  // Result of call to getMatchedSelectors
   1068  #matchedSelectorResponse = null;
   1069 
   1070  // Matched selector expando
   1071  #matchedExpander = null;
   1072 
   1073  // AbortController for event listeners
   1074  #abortController = null;
   1075 
   1076  // Cache for matched selector views
   1077  #matchedSelectorViews = null;
   1078 
   1079  // The previously selected element used for the selector view caches
   1080  #prevViewedElement = null;
   1081 
   1082  // PropertyInfo
   1083  #propertyInfo = null;
   1084 
   1085  #tree;
   1086 
   1087  /**
   1088   * Get the computed style for the current property.
   1089   *
   1090   * @return {string} the computed style for the current property of the
   1091   * currently highlighted element.
   1092   */
   1093  get value() {
   1094    return this.propertyInfo.value;
   1095  }
   1096 
   1097  /**
   1098   * An easy way to access the CssPropertyInfo behind this PropertyView.
   1099   */
   1100  get propertyInfo() {
   1101    return this.#propertyInfo;
   1102  }
   1103 
   1104  /**
   1105   * Does the property have any matched selectors?
   1106   */
   1107  get hasMatchedSelectors() {
   1108    return this.#tree.matchedProperties.has(this.name);
   1109  }
   1110 
   1111  /**
   1112   * Should this property be visible?
   1113   */
   1114  get visible() {
   1115    if (!this.#tree.viewedElement) {
   1116      return false;
   1117    }
   1118 
   1119    if (!this.#tree.includeBrowserStyles && !this.hasMatchedSelectors) {
   1120      return false;
   1121    }
   1122 
   1123    const searchTerm = this.#tree.searchField.value.toLowerCase();
   1124    const isValidSearchTerm = !!searchTerm.trim().length;
   1125    if (
   1126      isValidSearchTerm &&
   1127      !this.name.toLowerCase().includes(searchTerm) &&
   1128      !this.value.toLowerCase().includes(searchTerm)
   1129    ) {
   1130      return false;
   1131    }
   1132 
   1133    return this.propertyInfo.isSupported;
   1134  }
   1135 
   1136  /**
   1137   * Returns the className that should be assigned to the propertyView.
   1138   *
   1139   * @return {string}
   1140   */
   1141  get propertyHeaderClassName() {
   1142    return this.visible ? "computed-property-view" : "computed-property-hidden";
   1143  }
   1144 
   1145  /**
   1146   * Is the property invalid at computed value time
   1147   *
   1148   * @returns {boolean}
   1149   */
   1150  get invalidAtComputedValueTime() {
   1151    return this.#tree.computed[this.name].invalidAtComputedValueTime;
   1152  }
   1153 
   1154  /**
   1155   * If this is a registered property, returns its syntax (e.g. "<color>")
   1156   *
   1157   * @returns {Text|undefined}
   1158   */
   1159  get registeredPropertySyntax() {
   1160    return this.#tree.computed[this.name].registeredPropertySyntax;
   1161  }
   1162 
   1163  /**
   1164   * If this is a registered property, return its initial-value
   1165   *
   1166   * @returns {Text|undefined}
   1167   */
   1168  get registeredPropertyInitialValue() {
   1169    return this.#tree.computed[this.name].registeredPropertyInitialValue;
   1170  }
   1171 
   1172  /**
   1173   * Create DOM elements for a property
   1174   *
   1175   * @return {Element} The <li> element
   1176   */
   1177  createListItemElement() {
   1178    const doc = this.#tree.styleDocument;
   1179    const baseEventListenerConfig = { signal: this.#abortController.signal };
   1180 
   1181    // Build the container element
   1182    this.onMatchedToggle = this.onMatchedToggle.bind(this);
   1183    this.element = doc.createElement("li");
   1184    this.element.className = this.propertyHeaderClassName;
   1185    this.element.addEventListener(
   1186      "dblclick",
   1187      this.onMatchedToggle,
   1188      baseEventListenerConfig
   1189    );
   1190 
   1191    // Make it keyboard navigable
   1192    this.element.setAttribute("tabindex", "0");
   1193    this.shortcuts = new KeyShortcuts({
   1194      window: this.#tree.styleWindow,
   1195      target: this.element,
   1196    });
   1197    this.shortcuts.on("F1", event => {
   1198      this.mdnLinkClick(event);
   1199      // Prevent opening the options panel
   1200      event.preventDefault();
   1201      event.stopPropagation();
   1202    });
   1203    this.shortcuts.on("Return", this.onMatchedToggle);
   1204    this.shortcuts.on("Space", this.onMatchedToggle);
   1205 
   1206    const nameContainer = doc.createElement("span");
   1207    nameContainer.className = "computed-property-name-container";
   1208 
   1209    // Build the twisty expand/collapse
   1210    this.#matchedExpander = doc.createElement("div");
   1211    this.#matchedExpander.className = "computed-expander theme-twisty";
   1212    this.#matchedExpander.setAttribute("role", "button");
   1213    this.#matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL);
   1214    this.#matchedExpander.addEventListener(
   1215      "click",
   1216      this.onMatchedToggle,
   1217      baseEventListenerConfig
   1218    );
   1219 
   1220    // Build the style name element
   1221    const nameNode = doc.createElement("span");
   1222    nameNode.classList.add("computed-property-name", "theme-fg-color3");
   1223 
   1224    // Give it a heading role for screen readers.
   1225    nameNode.setAttribute("role", "heading");
   1226 
   1227    // Reset its tabindex attribute otherwise, if an ellipsis is applied
   1228    // it will be reachable via TABing
   1229    nameNode.setAttribute("tabindex", "");
   1230    // Avoid english text (css properties) from being altered
   1231    // by RTL mode
   1232    nameNode.setAttribute("dir", "ltr");
   1233    nameNode.textContent = nameNode.title = this.name;
   1234    // Make it hand over the focus to the container
   1235    const focusElement = () => this.element.focus();
   1236    nameNode.addEventListener("click", focusElement, baseEventListenerConfig);
   1237 
   1238    // Build the style name ":" separator
   1239    const nameSeparator = doc.createElement("span");
   1240    nameSeparator.classList.add("visually-hidden");
   1241    nameSeparator.textContent = ": ";
   1242    nameNode.appendChild(nameSeparator);
   1243 
   1244    nameContainer.appendChild(nameNode);
   1245 
   1246    const valueContainer = doc.createElement("span");
   1247    valueContainer.className = "computed-property-value-container";
   1248 
   1249    // Build the style value element
   1250    this.valueNode = doc.createElement("span");
   1251    this.valueNode.classList.add("computed-property-value", "theme-fg-color1");
   1252    // Reset its tabindex attribute otherwise, if an ellipsis is applied
   1253    // it will be reachable via TABing
   1254    this.valueNode.setAttribute("tabindex", "");
   1255    this.valueNode.setAttribute("dir", "ltr");
   1256    // Make it hand over the focus to the container
   1257    this.valueNode.addEventListener(
   1258      "click",
   1259      focusElement,
   1260      baseEventListenerConfig
   1261    );
   1262 
   1263    // Build the style value ";" separator
   1264    const valueSeparator = doc.createElement("span");
   1265    valueSeparator.classList.add("visually-hidden");
   1266    valueSeparator.textContent = ";";
   1267 
   1268    valueContainer.append(this.valueNode, valueSeparator);
   1269 
   1270    // If the value is invalid at computed value time (IACVT), display the same
   1271    // warning icon that we have in the rules view for IACVT declarations.
   1272    if (this.isCustomProperty) {
   1273      this.invalidAtComputedValueTimeNode = doc.createElement("div");
   1274      this.invalidAtComputedValueTimeNode.classList.add(
   1275        "invalid-at-computed-value-time-warning"
   1276      );
   1277      this.refreshInvalidAtComputedValueTime();
   1278      valueContainer.append(this.invalidAtComputedValueTimeNode);
   1279    }
   1280 
   1281    // Build the matched selectors container
   1282    this.matchedSelectorsContainer = doc.createElement("div");
   1283    this.matchedSelectorsContainer.classList.add("matchedselectors");
   1284 
   1285    this.element.append(
   1286      this.#matchedExpander,
   1287      nameContainer,
   1288      valueContainer,
   1289      this.matchedSelectorsContainer
   1290    );
   1291 
   1292    return this.element;
   1293  }
   1294 
   1295  /**
   1296   * Refresh the panel's CSS property value.
   1297   */
   1298  refresh() {
   1299    const className = this.propertyHeaderClassName;
   1300    if (this.element.className !== className) {
   1301      this.element.className = className;
   1302    }
   1303 
   1304    if (this.#prevViewedElement !== this.#tree.viewedElement) {
   1305      this.#matchedSelectorViews = null;
   1306      this.#prevViewedElement = this.#tree.viewedElement;
   1307    }
   1308 
   1309    if (!this.#tree.viewedElement || !this.visible) {
   1310      this.valueNode.textContent = this.valueNode.title = "";
   1311      this.matchedSelectorsContainer.parentNode.hidden = true;
   1312      this.matchedSelectorsContainer.textContent = "";
   1313      this.#matchedExpander.removeAttribute("open");
   1314      this.#matchedExpander.setAttribute(
   1315        "aria-label",
   1316        L10N_TWISTY_EXPAND_LABEL
   1317      );
   1318      return;
   1319    }
   1320 
   1321    this.#tree.numVisibleProperties++;
   1322 
   1323    this.valueNode.innerHTML = "";
   1324    // No need to pass the baseURI argument here as computed URIs are never relative.
   1325    this.valueNode.appendChild(this.#parseValue(this.propertyInfo.value));
   1326 
   1327    this.refreshInvalidAtComputedValueTime();
   1328    this.refreshMatchedSelectors();
   1329  }
   1330 
   1331  /**
   1332   * Refresh the panel matched rules.
   1333   */
   1334  refreshMatchedSelectors() {
   1335    const hasMatchedSelectors = this.hasMatchedSelectors;
   1336    this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
   1337 
   1338    if (hasMatchedSelectors) {
   1339      this.#matchedExpander.classList.add("computed-expandable");
   1340    } else {
   1341      this.#matchedExpander.classList.remove("computed-expandable");
   1342    }
   1343 
   1344    if (this.matchedExpanded && hasMatchedSelectors) {
   1345      return this.#tree.viewedElementPageStyle
   1346        .getMatchedSelectors(this.#tree.viewedElement, this.name)
   1347        .then(matched => {
   1348          if (!this.matchedExpanded) {
   1349            return;
   1350          }
   1351 
   1352          this.#matchedSelectorResponse = matched;
   1353 
   1354          this.#buildMatchedSelectors();
   1355          this.#matchedExpander.setAttribute("open", "");
   1356          this.#matchedExpander.setAttribute(
   1357            "aria-label",
   1358            L10N_TWISTY_COLLAPSE_LABEL
   1359          );
   1360          this.#tree.inspector.emit("computed-view-property-expanded");
   1361        })
   1362        .catch(console.error);
   1363    }
   1364 
   1365    this.matchedSelectorsContainer.innerHTML = "";
   1366    this.#matchedExpander.removeAttribute("open");
   1367    this.#matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL);
   1368    this.#tree.inspector.emit("computed-view-property-collapsed");
   1369    return Promise.resolve(undefined);
   1370  }
   1371 
   1372  /**
   1373   * Show/Hide IACVT icon and sets its title attribute
   1374   */
   1375  refreshInvalidAtComputedValueTime() {
   1376    if (!this.isCustomProperty) {
   1377      return;
   1378    }
   1379 
   1380    if (!this.invalidAtComputedValueTime) {
   1381      this.invalidAtComputedValueTimeNode.setAttribute("hidden", "");
   1382      this.invalidAtComputedValueTimeNode.removeAttribute("title");
   1383    } else {
   1384      this.invalidAtComputedValueTimeNode.removeAttribute("hidden", "");
   1385      this.invalidAtComputedValueTimeNode.setAttribute(
   1386        "title",
   1387        STYLE_INSPECTOR_L10N.getFormatStr(
   1388          "rule.warningInvalidAtComputedValueTime.title",
   1389          `"${this.registeredPropertySyntax}"`
   1390        )
   1391      );
   1392    }
   1393  }
   1394 
   1395  get matchedSelectors() {
   1396    return this.#matchedSelectorResponse;
   1397  }
   1398 
   1399  #buildMatchedSelectors() {
   1400    const frag = this.element.ownerDocument.createDocumentFragment();
   1401 
   1402    for (const selector of this.matchedSelectorViews) {
   1403      const p = createChild(frag, "p");
   1404      const span = createChild(p, "span", {
   1405        class: "rule-link",
   1406      });
   1407 
   1408      if (selector.source) {
   1409        const link = createChild(span, "a", {
   1410          target: "_blank",
   1411          class: "computed-link theme-link",
   1412          title: selector.longSource,
   1413          sourcelocation: selector.source,
   1414          tabindex: "0",
   1415          textContent: selector.source,
   1416        });
   1417        link.addEventListener("click", selector.openStyleEditor);
   1418        const shortcuts = new KeyShortcuts({
   1419          window: this.#tree.styleWindow,
   1420          target: link,
   1421        });
   1422        shortcuts.on("Return", () => selector.openStyleEditor());
   1423      }
   1424 
   1425      const status = createChild(p, "span", {
   1426        dir: "ltr",
   1427        class: "rule-text theme-fg-color3 " + selector.statusClass,
   1428        title: selector.statusText,
   1429      });
   1430 
   1431      // Add an explicit status text span for screen readers.
   1432      // They won't pick up the title from the status span.
   1433      createChild(status, "span", {
   1434        dir: "ltr",
   1435        class: "visually-hidden",
   1436        textContent: selector.statusText + " ",
   1437      });
   1438 
   1439      const selectorEl = createChild(status, "div", {
   1440        class: "fix-get-selection computed-other-property-selector",
   1441        textContent: selector.sourceText,
   1442      });
   1443      if (
   1444        selector.selectorInfo.rule.type === ELEMENT_STYLE ||
   1445        selector.selectorInfo.rule.type === PRES_HINTS
   1446      ) {
   1447        selectorEl.classList.add("alternative-selector");
   1448      }
   1449 
   1450      const valueDiv = createChild(status, "div", {
   1451        class:
   1452          "fix-get-selection computed-other-property-value theme-fg-color1",
   1453      });
   1454      valueDiv.appendChild(
   1455        this.#parseValue(
   1456          selector.selectorInfo.value,
   1457          selector.selectorInfo.rule.href
   1458        )
   1459      );
   1460 
   1461      // If the value is invalid at computed value time (IACVT), display the same
   1462      // warning icon that we have in the rules view for IACVT declarations.
   1463      if (selector.selectorInfo.invalidAtComputedValueTime) {
   1464        createChild(status, "div", {
   1465          class: "invalid-at-computed-value-time-warning",
   1466          title: STYLE_INSPECTOR_L10N.getFormatStr(
   1467            "rule.warningInvalidAtComputedValueTime.title",
   1468            `"${selector.selectorInfo.registeredPropertySyntax}"`
   1469          ),
   1470        });
   1471      }
   1472    }
   1473 
   1474    if (this.registeredPropertyInitialValue !== undefined) {
   1475      const p = createChild(frag, "p");
   1476      const status = createChild(p, "span", {
   1477        dir: "ltr",
   1478        class: "rule-text theme-fg-color3",
   1479      });
   1480 
   1481      createChild(status, "div", {
   1482        class: "fix-get-selection",
   1483        textContent: "initial-value",
   1484      });
   1485 
   1486      const valueDiv = createChild(status, "div", {
   1487        class:
   1488          "fix-get-selection computed-other-property-value theme-fg-color1",
   1489      });
   1490      valueDiv.appendChild(
   1491        this.#parseValue(this.registeredPropertyInitialValue)
   1492      );
   1493    }
   1494 
   1495    this.matchedSelectorsContainer.innerHTML = "";
   1496    this.matchedSelectorsContainer.appendChild(frag);
   1497  }
   1498 
   1499  /**
   1500   * Parse a property value using the OutputParser.
   1501   *
   1502   * @param {string} value
   1503   * @param {string} baseURI
   1504   * @returns {DocumentFragment|Element}
   1505   */
   1506  #parseValue(value, baseURI) {
   1507    if (this.isCustomProperty && value === "") {
   1508      const doc = this.#tree.styleDocument;
   1509      const el = doc.createElement("span");
   1510      el.classList.add("empty-css-variable");
   1511      el.append(doc.createTextNode(`<${L10N_EMPTY_VARIABLE}>`));
   1512      return el;
   1513    }
   1514 
   1515    // Sadly, because this fragment is added to the template by DOM Templater
   1516    // we lose any events that are attached. This means that URLs will open in a
   1517    // new window. At some point we should fix this by stopping using the
   1518    // templater.
   1519    return this.#tree.outputParser.parseCssProperty(this.name, value, {
   1520      colorSwatchClass: "inspector-swatch inspector-colorswatch",
   1521      colorSwatchReadOnly: true,
   1522      colorClass: "computed-color",
   1523      urlClass: "theme-link",
   1524      fontFamilyClass: "computed-font-family",
   1525      baseURI,
   1526    });
   1527  }
   1528 
   1529  /**
   1530   * Provide access to the matched SelectorViews that we are currently
   1531   * displaying.
   1532   */
   1533  get matchedSelectorViews() {
   1534    if (!this.#matchedSelectorViews) {
   1535      this.#matchedSelectorViews = [];
   1536      this.#matchedSelectorResponse.forEach(selectorInfo => {
   1537        const selectorView = new SelectorView(this.#tree, selectorInfo);
   1538        this.#matchedSelectorViews.push(selectorView);
   1539      }, this);
   1540    }
   1541    return this.#matchedSelectorViews;
   1542  }
   1543 
   1544  /**
   1545   * The action when a user expands matched selectors.
   1546   *
   1547   * @param {Event} event
   1548   *        Used to determine the class name of the targets click
   1549   *        event.
   1550   */
   1551  onMatchedToggle(event) {
   1552    if (event.shiftKey) {
   1553      return;
   1554    }
   1555    this.matchedExpanded = !this.matchedExpanded;
   1556    this.refreshMatchedSelectors();
   1557    event.preventDefault();
   1558  }
   1559 
   1560  /**
   1561   * The action when a user clicks on the MDN help link for a property.
   1562   */
   1563  mdnLinkClick() {
   1564    if (!this.link) {
   1565      return;
   1566    }
   1567    openContentLink(this.link);
   1568  }
   1569 
   1570  /**
   1571   * Destroy this property view, removing event listeners
   1572   */
   1573  destroy() {
   1574    if (this.#matchedSelectorViews) {
   1575      for (const view of this.#matchedSelectorViews) {
   1576        view.destroy();
   1577      }
   1578    }
   1579 
   1580    if (this.#abortController) {
   1581      this.#abortController.abort();
   1582      this.#abortController = null;
   1583    }
   1584 
   1585    if (this.shortcuts) {
   1586      this.shortcuts.destroy();
   1587    }
   1588 
   1589    this.shortcuts = null;
   1590    this.element = null;
   1591    this.#matchedExpander = null;
   1592    this.valueNode = null;
   1593  }
   1594 }
   1595 
   1596 /**
   1597 * A container to give us easy access to display data from a CssRule
   1598 */
   1599 class SelectorView {
   1600  /**
   1601   * @param CssComputedView tree
   1602   *        the owning CssComputedView
   1603   * @param selectorInfo
   1604   */
   1605  constructor(tree, selectorInfo) {
   1606    this.#tree = tree;
   1607    this.selectorInfo = selectorInfo;
   1608    this.#cacheStatusNames();
   1609 
   1610    this.openStyleEditor = this.openStyleEditor.bind(this);
   1611 
   1612    const rule = this.selectorInfo.rule;
   1613    if (rule?.parentStyleSheet) {
   1614      // This always refers to the generated location.
   1615      const sheet = rule.parentStyleSheet;
   1616      const sourceSuffix = rule.line > 0 ? ":" + rule.line : "";
   1617      this.source = CssLogic.shortSource(sheet) + sourceSuffix;
   1618      this.longSource = CssLogic.longSource(sheet) + sourceSuffix;
   1619 
   1620      this.#generatedLocation = {
   1621        sheet,
   1622        href: sheet.href || sheet.nodeHref,
   1623        line: rule.line,
   1624        column: rule.column,
   1625      };
   1626      this.#unsubscribeCallback =
   1627        this.#tree.inspector.toolbox.sourceMapURLService.subscribeByID(
   1628          this.#generatedLocation.sheet.resourceId,
   1629          this.#generatedLocation.line,
   1630          this.#generatedLocation.column,
   1631          this.#updateLocation
   1632        );
   1633    }
   1634  }
   1635 
   1636  #generatedLocation;
   1637  #href;
   1638  #tree;
   1639  #unsubscribeCallback;
   1640 
   1641  /**
   1642   * Decode for cssInfo.rule.status
   1643   *
   1644   * @see SelectorView.prototype.#cacheStatusNames
   1645   * @see CssLogic.STATUS
   1646   */
   1647  static STATUS_NAMES = [
   1648    // "Parent Match", "Matched", "Best Match"
   1649  ];
   1650 
   1651  static CLASS_NAMES = ["parentmatch", "matched", "bestmatch"];
   1652 
   1653  /**
   1654   * Cache localized status names.
   1655   *
   1656   * These statuses are localized inside the styleinspector.properties string
   1657   * bundle.
   1658   *
   1659   * @see css-logic.js - the CssLogic.STATUS array.
   1660   */
   1661  #cacheStatusNames() {
   1662    if (SelectorView.STATUS_NAMES.length) {
   1663      return;
   1664    }
   1665 
   1666    for (const status in CssLogic.STATUS) {
   1667      const i = CssLogic.STATUS[status];
   1668      if (i > CssLogic.STATUS.UNMATCHED) {
   1669        const value = CssComputedView.l10n("rule.status." + status);
   1670        // Replace normal spaces with non-breaking spaces
   1671        SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
   1672      }
   1673    }
   1674  }
   1675 
   1676  /**
   1677   * A localized version of cssRule.status
   1678   */
   1679  get statusText() {
   1680    return SelectorView.STATUS_NAMES[this.selectorInfo.status];
   1681  }
   1682 
   1683  /**
   1684   * Get class name for selector depending on status
   1685   */
   1686  get statusClass() {
   1687    return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
   1688  }
   1689 
   1690  get href() {
   1691    if (this.#href) {
   1692      return this.#href;
   1693    }
   1694    const sheet = this.selectorInfo.rule.parentStyleSheet;
   1695    this.#href = sheet ? sheet.href : "#";
   1696    return this.#href;
   1697  }
   1698 
   1699  get sourceText() {
   1700    return this.selectorInfo.sourceText;
   1701  }
   1702 
   1703  get value() {
   1704    return this.selectorInfo.value;
   1705  }
   1706 
   1707  /**
   1708   * Update the text of the source link to reflect whether we're showing
   1709   * original sources or not.  This is a callback for
   1710   * SourceMapURLService.subscribe, which see.
   1711   *
   1712   * @param {object | null} originalLocation
   1713   *        The original position object (url/line/column) or null.
   1714   */
   1715  #updateLocation = originalLocation => {
   1716    if (!this.#tree.element) {
   1717      return;
   1718    }
   1719 
   1720    // Update |currentLocation| to be whichever location is being
   1721    // displayed at the moment.
   1722    let currentLocation = this.#generatedLocation;
   1723    if (originalLocation) {
   1724      const { url, line, column } = originalLocation;
   1725      currentLocation = { href: url, line, column };
   1726    }
   1727 
   1728    const selector = '[sourcelocation="' + this.source + '"]';
   1729    const link = this.#tree.element.querySelector(selector);
   1730    if (link) {
   1731      const text =
   1732        CssLogic.shortSource(currentLocation) + ":" + currentLocation.line;
   1733      link.textContent = text;
   1734    }
   1735 
   1736    this.#tree.inspector.emit("computed-view-sourcelinks-updated");
   1737  };
   1738 
   1739  /**
   1740   * When a css link is clicked this method is called in order to either:
   1741   *   1. Open the link in view source (for chrome stylesheets).
   1742   *   2. Open the link in the style editor.
   1743   *
   1744   *   We can only view stylesheets contained in document.styleSheets inside the
   1745   *   style editor.
   1746   */
   1747  openStyleEditor() {
   1748    const inspector = this.#tree.inspector;
   1749    const rule = this.selectorInfo.rule;
   1750 
   1751    // The style editor can only display stylesheets coming from content because
   1752    // chrome stylesheets are not listed in the editor's stylesheet selector.
   1753    //
   1754    // If the stylesheet is a content stylesheet we send it to the style
   1755    // editor else we display it in the view source window.
   1756    const parentStyleSheet = rule.parentStyleSheet;
   1757    if (!parentStyleSheet || parentStyleSheet.isSystem) {
   1758      inspector.toolbox.viewSource(rule.href, rule.line);
   1759      return;
   1760    }
   1761 
   1762    const { sheet, line, column } = this.#generatedLocation;
   1763    if (ToolDefinitions.styleEditor.isToolSupported(inspector.toolbox)) {
   1764      inspector.toolbox.viewSourceInStyleEditorByResource(sheet, line, column);
   1765    }
   1766  }
   1767 
   1768  /**
   1769   * Destroy this selector view, removing event listeners
   1770   */
   1771  destroy() {
   1772    if (this.#unsubscribeCallback) {
   1773      this.#unsubscribeCallback();
   1774    }
   1775  }
   1776 }
   1777 
   1778 class ComputedViewTool {
   1779  /**
   1780   * @param {Inspector} inspector
   1781   * @param {Window} window
   1782   */
   1783  constructor(inspector, window) {
   1784    this.inspector = inspector;
   1785    this.document = window.document;
   1786 
   1787    this.computedView = new CssComputedView(this.inspector, this.document);
   1788 
   1789    this.onDetachedFront = this.onDetachedFront.bind(this);
   1790    this.onSelected = this.onSelected.bind(this);
   1791    this.refresh = this.refresh.bind(this);
   1792    this.onPanelSelected = this.onPanelSelected.bind(this);
   1793 
   1794    this.#abortController = new AbortController();
   1795    const opts = { signal: this.#abortController.signal };
   1796    this.inspector.selection.on("detached-front", this.onDetachedFront, opts);
   1797    this.inspector.selection.on("new-node-front", this.onSelected, opts);
   1798    this.inspector.selection.on("pseudoclass", this.refresh, opts);
   1799    this.inspector.sidebar.on(
   1800      "computedview-selected",
   1801      this.onPanelSelected,
   1802      opts
   1803    );
   1804    this.inspector.styleChangeTracker.on(
   1805      "style-changed",
   1806      () => {
   1807        // `refresh` may not actually update the styles if the computed panel is hidden
   1808        // so use a flag to force updating the element styles the next time the computed
   1809        // panel refreshes.
   1810        this.computedView.elementStyleUpdated = true;
   1811        this.refresh();
   1812      },
   1813      opts
   1814    );
   1815 
   1816    this.computedView.selectElement(null);
   1817 
   1818    this.onSelected();
   1819  }
   1820 
   1821  #abortController;
   1822 
   1823  isPanelVisible() {
   1824    if (!this.computedView) {
   1825      return false;
   1826    }
   1827    return this.computedView.isPanelVisible();
   1828  }
   1829 
   1830  onDetachedFront() {
   1831    this.onSelected(false);
   1832  }
   1833 
   1834  async onSelected(selectElement = true) {
   1835    // Ignore the event if the view has been destroyed, or if it's inactive.
   1836    // But only if the current selection isn't null. If it's been set to null,
   1837    // let the update go through as this is needed to empty the view on
   1838    // navigation.
   1839    if (!this.computedView) {
   1840      return;
   1841    }
   1842 
   1843    const isInactive =
   1844      !this.isPanelVisible() && this.inspector.selection.nodeFront;
   1845    if (isInactive) {
   1846      return;
   1847    }
   1848 
   1849    if (
   1850      !this.inspector.selection.isConnected() ||
   1851      !this.inspector.selection.isElementNode()
   1852    ) {
   1853      this.computedView.selectElement(null);
   1854      return;
   1855    }
   1856 
   1857    if (selectElement) {
   1858      const done = this.inspector.updating("computed-view");
   1859      await this.computedView.selectElement(this.inspector.selection.nodeFront);
   1860      done();
   1861    }
   1862  }
   1863 
   1864  refresh() {
   1865    if (this.isPanelVisible()) {
   1866      this.computedView.refreshPanel();
   1867    }
   1868  }
   1869 
   1870  onPanelSelected() {
   1871    if (
   1872      this.inspector.selection.nodeFront === this.computedView.viewedElement
   1873    ) {
   1874      this.refresh();
   1875    } else {
   1876      this.onSelected();
   1877    }
   1878  }
   1879 
   1880  destroy() {
   1881    this.#abortController.abort();
   1882    this.computedView.destroy();
   1883 
   1884    this.computedView =
   1885      this.document =
   1886      this.inspector =
   1887      this.#abortController =
   1888        null;
   1889  }
   1890 }
   1891 
   1892 exports.CssComputedView = CssComputedView;
   1893 exports.ComputedViewTool = ComputedViewTool;
   1894 exports.PropertyView = PropertyView;