tor-browser

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

inspector.js (68182B)


      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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const flags = require("resource://devtools/shared/flags.js");
      9 const { executeSoon } = require("resource://devtools/shared/DevToolsUtils.js");
     10 const { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
     11 const createStore = require("resource://devtools/client/inspector/store.js");
     12 const InspectorStyleChangeTracker = require("resource://devtools/client/inspector/shared/style-change-tracker.js");
     13 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
     14 const {
     15  START_IGNORE_ACTION,
     16 } = require("resource://devtools/client/shared/redux/middleware/ignore.js");
     17 
     18 // Use privileged promise in panel documents to prevent having them to freeze
     19 // during toolbox destruction. See bug 1402779.
     20 const Promise = require("Promise");
     21 const osString = Services.appinfo.OS;
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  "HTMLBreadcrumbs",
     26  "resource://devtools/client/inspector/breadcrumbs.js",
     27  true
     28 );
     29 loader.lazyRequireGetter(
     30  this,
     31  "KeyShortcuts",
     32  "resource://devtools/client/shared/key-shortcuts.js"
     33 );
     34 loader.lazyRequireGetter(
     35  this,
     36  "InspectorSearch",
     37  "resource://devtools/client/inspector/inspector-search.js",
     38  true
     39 );
     40 loader.lazyRequireGetter(
     41  this,
     42  "ToolSidebar",
     43  "resource://devtools/client/inspector/toolsidebar.js",
     44  true
     45 );
     46 loader.lazyRequireGetter(
     47  this,
     48  "MarkupView",
     49  "resource://devtools/client/inspector/markup/markup.js"
     50 );
     51 loader.lazyRequireGetter(
     52  this,
     53  "HighlightersOverlay",
     54  "resource://devtools/client/inspector/shared/highlighters-overlay.js"
     55 );
     56 loader.lazyRequireGetter(
     57  this,
     58  "PICKER_TYPES",
     59  "resource://devtools/shared/picker-constants.js"
     60 );
     61 loader.lazyRequireGetter(
     62  this,
     63  "captureAndSaveScreenshot",
     64  "resource://devtools/client/shared/screenshot.js",
     65  true
     66 );
     67 loader.lazyRequireGetter(
     68  this,
     69  "debounce",
     70  "resource://devtools/shared/debounce.js",
     71  true
     72 );
     73 
     74 const {
     75  LocalizationHelper,
     76  localizeMarkup,
     77 } = require("resource://devtools/shared/l10n.js");
     78 const INSPECTOR_L10N = new LocalizationHelper(
     79  "devtools/client/locales/inspector.properties"
     80 );
     81 const {
     82  FluentL10n,
     83 } = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
     84 
     85 // Sidebar dimensions
     86 const INITIAL_SIDEBAR_SIZE = 350;
     87 
     88 // How long we wait to debounce resize events
     89 const LAZY_RESIZE_INTERVAL_MS = 200;
     90 
     91 // If the toolbox's width is smaller than the given amount of pixels, the sidebar
     92 // automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
     93 const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
     94 // If the toolbox's width docked to the side is smaller than the given amount of pixels,
     95 // the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
     96 // mode.
     97 const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
     98 
     99 const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
    100 const THREE_PANE_CHROME_ENABLED_PREF =
    101  "devtools.inspector.chrome.three-pane-enabled";
    102 const DEFAULT_COLOR_UNIT_PREF = "devtools.defaultColorUnit";
    103 
    104 /**
    105 * Represents an open instance of the Inspector for a tab.
    106 * The inspector controls the breadcrumbs, the markup view, and the sidebar
    107 * (computed view, rule view, font view and animation inspector).
    108 *
    109 * Events:
    110 * - ready
    111 *      Fired when the inspector panel is opened for the first time and ready to
    112 *      use
    113 * - new-root
    114 *      Fired after a new root (navigation to a new page) event was fired by
    115 *      the walker, and taken into account by the inspector (after the markup
    116 *      view has been reloaded)
    117 * - markuploaded
    118 *      Fired when the markup-view frame has loaded
    119 * - breadcrumbs-updated
    120 *      Fired when the breadcrumb widget updates to a new node
    121 * - boxmodel-view-updated
    122 *      Fired when the box model updates to a new node
    123 * - markupmutation
    124 *      Fired after markup mutations have been processed by the markup-view
    125 * - computed-view-refreshed
    126 *      Fired when the computed rules view updates to a new node
    127 * - computed-view-property-expanded
    128 *      Fired when a property is expanded in the computed rules view
    129 * - computed-view-property-collapsed
    130 *      Fired when a property is collapsed in the computed rules view
    131 * - computed-view-sourcelinks-updated
    132 *      Fired when the stylesheet source links have been updated (when switching
    133 *      to source-mapped files)
    134 * - rule-view-refreshed
    135 *      Fired when the rule view updates to a new node
    136 * - rule-view-sourcelinks-updated
    137 *      Fired when the stylesheet source links have been updated (when switching
    138 *      to source-mapped files)
    139 */
    140 class Inspector extends EventEmitter {
    141  constructor(toolbox, commands, win) {
    142    super();
    143 
    144    this.#toolbox = toolbox;
    145    this.#commands = commands;
    146    this.panelDoc = win.document;
    147    this.panelWin = win;
    148    this.panelWin.inspector = this;
    149    this.telemetry = toolbox.telemetry;
    150    this.store = createStore(this);
    151 
    152    this.onResourceAvailable = this.onResourceAvailable.bind(this);
    153    this.onRootNodeAvailable = this.onRootNodeAvailable.bind(this);
    154    this.onPickerCanceled = this.onPickerCanceled.bind(this);
    155    this.onPickerHovered = this.onPickerHovered.bind(this);
    156    this.onPickerPicked = this.onPickerPicked.bind(this);
    157    this.onSidebarHidden = this.onSidebarHidden.bind(this);
    158    this.onSidebarResized = this.onSidebarResized.bind(this);
    159    this.onSidebarSelect = this.onSidebarSelect.bind(this);
    160    this.onSidebarShown = this.onSidebarShown.bind(this);
    161    this.onSidebarToggle = this.onSidebarToggle.bind(this);
    162    this.addNode = this.addNode.bind(this);
    163    this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
    164    this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
    165 
    166    this.prefObserver = new PrefObserver("devtools.");
    167    this.prefObserver.on(
    168      DEFAULT_COLOR_UNIT_PREF,
    169      this.#handleDefaultColorUnitPrefChange
    170    );
    171    this.defaultColorUnit = Services.prefs.getStringPref(
    172      DEFAULT_COLOR_UNIT_PREF
    173    );
    174  }
    175 
    176  #toolbox;
    177  #commands;
    178  // Map [panel id => panel instance]
    179  // Stores all the instances of sidebar panels like rule view, computed view, ...
    180  #panels = new Map();
    181  #fluentL10n;
    182  #defaultStartupNode;
    183  #defaultStartupNodeDomReference;
    184  #defaultStartupNodeSelectionReason;
    185  #defaultNode;
    186  #watchedResources;
    187  #highlighters;
    188  #newRootStart;
    189  #markupFrame;
    190  #markupBox;
    191  #isThreePaneModeEnabled;
    192  #search;
    193  #cssProperties;
    194  #destroyed;
    195  #pendingSelectionUnique;
    196  #InspectorTabPanel;
    197  #InspectorSplitBox;
    198  #TabBar;
    199  #updateProgress;
    200 
    201  /**
    202   * InspectorPanel.open() is effectively an asynchronous constructor.
    203   * Set any attributes or listeners that rely on the document being loaded or fronts
    204   * from the InspectorFront and Target here.
    205   *
    206   * @param {object} options
    207   * @param {NodeFront|undefined} options.defaultStartupNode: Optional node front that
    208   *        will be selected when the first root node is available.
    209   * @param {ElementIdentifier|undefined} options.defaultStartupNodeDomReference: Optional
    210   *        element identifier whose matching node front will be selected when the first
    211   *        root node is available.
    212   *        Will be ignored if defaultStartupNode is passed.
    213   * @param {string | undefined} options.defaultStartupNodeSelectionReason: Optional string
    214   *        that will be used as a reason for the node selection when either
    215   *        defaultStartupNode or defaultStartupNodeDomReference is passed
    216   * @returns {Inspector}
    217   */
    218  async init(options = {}) {
    219    // Localize all the nodes containing a data-localization attribute.
    220    localizeMarkup(this.panelDoc);
    221 
    222    this.#fluentL10n = new FluentL10n();
    223    await this.#fluentL10n.init(["devtools/client/compatibility.ftl"]);
    224 
    225    // Add the class that will display the main inspector panel with: search input,
    226    // markup view and breadcrumbs.
    227    this.panelDoc
    228      .getElementById("inspector-main-content")
    229      .classList.add("initialized");
    230 
    231    // Setup the splitter before watching targets & resources.
    232    // The markup view will be initialized after we get the first root-node
    233    // resource, and the splitter should be initialized before that.
    234    // The markup view is rendered in an iframe and the splitter will move the
    235    // parent of the iframe in the DOM tree which would reset the state of the
    236    // iframe if it had already been initialized.
    237    this.#setupSplitter();
    238 
    239    // Optional NodeFront/ElementIdentifier set on inspector startup, to be selected once the first root
    240    // node is available.
    241    this.#defaultStartupNode = options.defaultStartupNode;
    242    this.#defaultStartupNodeDomReference =
    243      options.defaultStartupNodeDomReference;
    244    this.#defaultStartupNodeSelectionReason =
    245      options.defaultStartupNodeSelectionReason;
    246 
    247    // NodeFront for the DOM Element selected when opening the inspector, or after each
    248    // navigation (i.e. each time a new Root Node is available)
    249    // This is used as a fallback if the currently selected node is removed.
    250    this.#defaultNode = null;
    251 
    252    await this.commands.targetCommand.watchTargets({
    253      types: [this.commands.targetCommand.TYPES.FRAME],
    254      onAvailable: this.#onTargetAvailable,
    255      onSelected: this.#onTargetSelected,
    256      onDestroyed: this.#onTargetDestroyed,
    257    });
    258 
    259    const { TYPES } = this.commands.resourceCommand;
    260    this.#watchedResources = [
    261      // To observe CSS change before opening changes view.
    262      TYPES.CSS_CHANGE,
    263      TYPES.DOCUMENT_EVENT,
    264      TYPES.REFLOW,
    265    ];
    266    // The root node is retrieved from onTargetSelected which is now called
    267    // on startup as well as on any navigation (= new top level target).
    268    //
    269    // We only listen to new root node in the browser toolbox, which is the last
    270    // configuration to use one target for multiple window global.
    271    const isBrowserToolbox =
    272      this.commands.descriptorFront.isBrowserProcessDescriptor;
    273    if (isBrowserToolbox) {
    274      this.#watchedResources.push(TYPES.ROOT_NODE);
    275    }
    276 
    277    await this.commands.resourceCommand.watchResources(this.#watchedResources, {
    278      onAvailable: this.onResourceAvailable,
    279    });
    280 
    281    // Store the URL of the target page prior to navigation in order to ensure
    282    // telemetry counts in the Grid Inspector are not double counted on reload.
    283    this.previousURL = this.currentTarget.url;
    284 
    285    // Note: setupSidebar() really has to be called after the first target has
    286    // been processed, so that the cssProperties getter works.
    287    // But the rest could be moved before the watch* calls.
    288    this.styleChangeTracker = new InspectorStyleChangeTracker(this);
    289    this.#setupSidebar();
    290    this.breadcrumbs = new HTMLBreadcrumbs(this);
    291    this.#setupExtensionSidebars();
    292    this.#setupSearchBox();
    293    this.#createInspectorShortcuts();
    294 
    295    this.#onNewSelection();
    296 
    297    this.toolbox.on("host-changed", this.#onHostChanged);
    298    this.toolbox.nodePicker.on("picker-node-hovered", this.onPickerHovered);
    299    this.toolbox.nodePicker.on("picker-node-canceled", this.onPickerCanceled);
    300    this.toolbox.nodePicker.on("picker-node-picked", this.onPickerPicked);
    301    this.selection.on("new-node-front", this.#onNewSelection);
    302    this.selection.on("detached-front", this.#onDetached);
    303 
    304    // Log the 3 pane inspector setting on inspector open. The question we want to answer
    305    // is:
    306    // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
    307    Glean.devtoolsInspector.threePaneEnabled[this.isThreePaneModeEnabled].add(
    308      1
    309    );
    310 
    311    return this;
    312  }
    313 
    314  // The onTargetAvailable argument is mandatory for TargetCommand.watchTargets.
    315  // The inspector ignore all targets but the currently selected one,
    316  // so all the target work is done from onTargetSelected.
    317  #onTargetAvailable = async ({ targetFront }) => {
    318    if (!targetFront.isTopLevel) {
    319      return;
    320    }
    321 
    322    // Fetch data and fronts which aren't WindowGlobal specific
    323    // and can be fetched once from the top level target.
    324    await Promise.all([
    325      this.#getCssProperties(targetFront),
    326      this.#getAccessibilityFront(targetFront),
    327    ]);
    328  };
    329 
    330  #onTargetSelected = async ({ targetFront }) => {
    331    // We don't use this.highlighters since it creates a HighlightersOverlay if it wasn't
    332    // the case yet.
    333    if (this.#highlighters) {
    334      this.#highlighters.hideAllHighlighters();
    335    }
    336    if (targetFront.isDestroyed()) {
    337      return;
    338    }
    339 
    340    await this.#initInspectorFront(targetFront);
    341 
    342    // the target might have been destroyed when reloading quickly,
    343    // while waiting for inspector front initialization
    344    if (targetFront.isDestroyed()) {
    345      return;
    346    }
    347 
    348    const { walker } = await targetFront.getFront("inspector");
    349    const rootNodeFront = await walker.getRootNode();
    350 
    351    // onRootNodeAvailable will take care of populating the markup view
    352    await this.onRootNodeAvailable(rootNodeFront);
    353  };
    354 
    355  #onTargetDestroyed = ({ targetFront }) => {
    356    // Ignore all targets but the top level one
    357    if (!targetFront.isTopLevel) {
    358      return;
    359    }
    360 
    361    this.#defaultNode = null;
    362    this.selection.setNodeFront(null);
    363  };
    364 
    365  onResourceAvailable(resources) {
    366    // Store all onRootNodeAvailable calls which are asynchronous.
    367    const rootNodeAvailablePromises = [];
    368 
    369    for (const resource of resources) {
    370      const isTopLevelTarget = !!resource.targetFront?.isTopLevel;
    371      const isTopLevelDocument = !!resource.isTopLevelDocument;
    372 
    373      if (
    374        resource.resourceType ===
    375          this.commands.resourceCommand.TYPES.ROOT_NODE &&
    376        // It might happen that the ROOT_NODE resource (which is a Front) is already
    377        // destroyed, and in such case we want to ignore it.
    378        !resource.isDestroyed() &&
    379        isTopLevelTarget &&
    380        isTopLevelDocument
    381      ) {
    382        rootNodeAvailablePromises.push(this.onRootNodeAvailable(resource));
    383      }
    384 
    385      // Only consider top level document, and ignore remote iframes top document
    386      if (
    387        resource.resourceType ===
    388          this.commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
    389        resource.name === "will-navigate" &&
    390        isTopLevelTarget
    391      ) {
    392        this.#onWillNavigate();
    393      }
    394 
    395      if (
    396        resource.resourceType === this.commands.resourceCommand.TYPES.REFLOW
    397      ) {
    398        this.emit("reflow");
    399        if (resource.targetFront === this.selection?.nodeFront?.targetFront) {
    400          // This event will be fired whenever a reflow is detected in the target front of the
    401          // selected node front (so when a reflow is detected inside any of the windows that
    402          // belong to the BrowsingContext where the currently selected node lives).
    403          this.emit("reflow-in-selected-target");
    404        }
    405      }
    406    }
    407 
    408    return Promise.all(rootNodeAvailablePromises);
    409  }
    410 
    411  /**
    412   * Reset the inspector on new root mutation.
    413   */
    414  async onRootNodeAvailable(rootNodeFront) {
    415    // Record new-root timing for telemetry
    416    this.#newRootStart = this.panelWin.performance.now();
    417 
    418    this.selection.setNodeFront(null);
    419    this.#destroyMarkup();
    420 
    421    try {
    422      const defaultNode = await this.#getDefaultNodeForSelection(rootNodeFront);
    423      if (!defaultNode) {
    424        return;
    425      }
    426 
    427      this.selection.setNodeFront(defaultNode, {
    428        reason:
    429          this.#defaultStartupNodeSelectionReason ??
    430          "inspector-default-selection",
    431      });
    432      this.#defaultStartupNodeSelectionReason = null;
    433 
    434      await this.#initMarkupView();
    435 
    436      // Setup the toolbar again, since its content may depend on the current document.
    437      this.#setupToolbar();
    438    } catch (e) {
    439      this.#handleRejectionIfNotDestroyed(e);
    440    }
    441  }
    442 
    443  async #initMarkupView() {
    444    if (!this.#markupFrame) {
    445      this.#markupFrame = this.panelDoc.createElement("iframe");
    446      this.#markupFrame.setAttribute(
    447        "aria-label",
    448        INSPECTOR_L10N.getStr("inspector.panelLabel.markupView")
    449      );
    450      this.#markupFrame.setAttribute("flex", "1");
    451      // This is needed to enable tooltips inside the iframe document.
    452      this.#markupFrame.setAttribute("tooltip", "aHTMLTooltip");
    453 
    454      this.#markupBox = this.panelDoc.getElementById("markup-box");
    455      this.#markupBox.style.visibility = "hidden";
    456      this.#markupBox.appendChild(this.#markupFrame);
    457 
    458      const onMarkupFrameLoaded = new Promise(r =>
    459        this.#markupFrame.addEventListener("load", r, {
    460          capture: true,
    461          once: true,
    462        })
    463      );
    464 
    465      this.#markupFrame.setAttribute("src", "markup/markup.xhtml");
    466 
    467      await onMarkupFrameLoaded;
    468    }
    469 
    470    this.#markupFrame.contentWindow.focus();
    471    this.#markupBox.style.visibility = "visible";
    472    this.markup = new MarkupView(this, this.#markupFrame, this.#toolbox.win);
    473    // TODO: We might be able to merge markuploaded, new-root and reloaded.
    474    this.emitForTests("markuploaded");
    475 
    476    const onExpand = this.markup.expandNode(this.selection.nodeFront);
    477 
    478    // Restore the highlighter states prior to emitting "new-root".
    479    if (this.#highlighters) {
    480      await Promise.all([
    481        this.highlighters.restoreFlexboxState(),
    482        this.highlighters.restoreGridState(),
    483      ]);
    484    }
    485    this.emit("new-root");
    486 
    487    // Wait for full expand of the selected node in order to ensure
    488    // the markup view is fully emitted before firing 'reloaded'.
    489    // 'reloaded' is used to know when the panel is fully updated
    490    // after a page reload.
    491    await onExpand;
    492 
    493    this.emit("reloaded");
    494 
    495    // Record the time between new-root event and inspector fully loaded.
    496    if (this.#newRootStart) {
    497      // Only log the timing when inspector is not destroyed and is in foreground.
    498      if (this.toolbox && this.toolbox.currentToolId == "inspector") {
    499        const delay = this.panelWin.performance.now() - this.#newRootStart;
    500        Glean.devtoolsInspector.newRootToReloadDelay.accumulateSingleSample(
    501          delay
    502        );
    503      }
    504      this.#newRootStart = null;
    505    }
    506  }
    507 
    508  async #initInspectorFront(targetFront) {
    509    this.inspectorFront = await targetFront.getFront("inspector");
    510    this.walker = this.inspectorFront.walker;
    511  }
    512 
    513  get toolbox() {
    514    return this.#toolbox;
    515  }
    516 
    517  get commands() {
    518    return this.#commands;
    519  }
    520 
    521  /**
    522   * Get the list of InspectorFront instances that correspond to all of the inspectable
    523   * targets in remote frames nested within the document inspected here, as well as the
    524   * current InspectorFront instance.
    525   *
    526   * @return {Array} The list of InspectorFront instances.
    527   */
    528  async getAllInspectorFronts() {
    529    return this.commands.targetCommand.getAllFronts(
    530      [this.commands.targetCommand.TYPES.FRAME],
    531      "inspector"
    532    );
    533  }
    534 
    535  get highlighters() {
    536    if (!this.#highlighters) {
    537      this.#highlighters = new HighlightersOverlay(this);
    538    }
    539 
    540    return this.#highlighters;
    541  }
    542 
    543  get #threePanePrefName() {
    544    // All other contexts: webextension and browser toolbox
    545    // are considered as "chrome"
    546    return this.commands.descriptorFront.isTabDescriptor
    547      ? THREE_PANE_ENABLED_PREF
    548      : THREE_PANE_CHROME_ENABLED_PREF;
    549  }
    550 
    551  get isThreePaneModeEnabled() {
    552    if (!this.#isThreePaneModeEnabled) {
    553      this.#isThreePaneModeEnabled = Services.prefs.getBoolPref(
    554        this.#threePanePrefName
    555      );
    556    }
    557    return this.#isThreePaneModeEnabled;
    558  }
    559 
    560  set isThreePaneModeEnabled(value) {
    561    this.#isThreePaneModeEnabled = value;
    562    Services.prefs.setBoolPref(
    563      this.#threePanePrefName,
    564      this.#isThreePaneModeEnabled
    565    );
    566  }
    567 
    568  get search() {
    569    if (!this.#search) {
    570      this.#search = new InspectorSearch(
    571        this,
    572        this.searchBox,
    573        this.searchClearButton,
    574        this.searchPrevButton,
    575        this.searchNextButton
    576      );
    577    }
    578 
    579    return this.#search;
    580  }
    581 
    582  get selection() {
    583    return this.toolbox.selection;
    584  }
    585 
    586  get cssProperties() {
    587    return this.#cssProperties.cssProperties;
    588  }
    589 
    590  get fluentL10n() {
    591    return this.#fluentL10n;
    592  }
    593 
    594  // Duration in milliseconds after which to hide the highlighter for the picked node.
    595  // While testing, disable auto hiding to prevent intermittent test failures.
    596  // Some tests are very slow. If the highlighter is hidden after a delay, the test may
    597  // find itself midway through without a highlighter to test.
    598  // This value is exposed on Inspector so individual tests can restore it when needed.
    599  HIGHLIGHTER_AUTOHIDE_TIMER = flags.testing ? 0 : 1000;
    600 
    601  #handleDefaultColorUnitPrefChange = () => {
    602    this.defaultColorUnit = Services.prefs.getStringPref(
    603      DEFAULT_COLOR_UNIT_PREF
    604    );
    605  };
    606 
    607  /**
    608   * Handle promise rejections for various asynchronous actions, and only log errors if
    609   * the inspector panel still exists.
    610   * This is useful to silence useless errors that happen when the inspector is closed
    611   * while still initializing (and making protocol requests).
    612   */
    613  #handleRejectionIfNotDestroyed = e => {
    614    if (!this.#destroyed) {
    615      console.error(e);
    616    }
    617  };
    618 
    619  #onWillNavigate = () => {
    620    this.#defaultNode = null;
    621    this.selection.setNodeFront(null);
    622    if (this.#highlighters) {
    623      this.#highlighters.hideAllHighlighters();
    624    }
    625    this.#destroyMarkup();
    626    this.#pendingSelectionUnique = null;
    627  };
    628 
    629  async #getCssProperties(targetFront) {
    630    this.#cssProperties = await targetFront.getFront("cssProperties");
    631  }
    632 
    633  async #getAccessibilityFront(targetFront) {
    634    this.accessibilityFront = await targetFront.getFront("accessibility");
    635    return this.accessibilityFront;
    636  }
    637 
    638  /**
    639   * Return a promise that will resolve to the default node for selection.
    640   *
    641   * @param {NodeFront} rootNodeFront
    642   *        The current root node front for the top walker.
    643   */
    644  async #getDefaultNodeForSelection(rootNodeFront) {
    645    let node;
    646    if (this.#defaultStartupNode) {
    647      node = this.#defaultStartupNode;
    648      this.#defaultStartupNode = null;
    649      this.#defaultStartupNodeDomReference = null;
    650      return node;
    651    }
    652 
    653    // Save the _pendingSelectionUnique on the current inspector instance.
    654    const pendingSelectionUnique = Symbol("pending-selection");
    655    this.#pendingSelectionUnique = pendingSelectionUnique;
    656 
    657    if (this.#defaultStartupNodeDomReference) {
    658      const domReference = this.#defaultStartupNodeDomReference;
    659      // nullify before calling the async getNodeActorFromContentDomReference so calls
    660      // made to getDefaultNodeForSelection while the promise is pending will be properly
    661      // ignored with the check on pendingSelectionUnique
    662      this.#defaultStartupNode = null;
    663      this.#defaultStartupNodeDomReference = null;
    664 
    665      try {
    666        node =
    667          await this.inspectorFront.getNodeActorFromContentDomReference(
    668            domReference
    669          );
    670      } catch (e) {
    671        console.warn(
    672          "Couldn't retrieve node front from dom reference",
    673          domReference
    674        );
    675      }
    676    }
    677 
    678    if (this.#pendingSelectionUnique !== pendingSelectionUnique) {
    679      // If this method was called again while waiting, bail out.
    680      return null;
    681    }
    682 
    683    if (node) {
    684      return node;
    685    }
    686 
    687    const walker = rootNodeFront.walkerFront;
    688    const cssSelectors = this.selectionCssSelectors;
    689    // Try to find a default node using three strategies:
    690    const defaultNodeSelectors = [
    691      // - first try to match css selectors for the selection
    692      () =>
    693        cssSelectors.length
    694          ? this.commands.inspectorCommand.findNodeFrontFromSelectors(
    695              cssSelectors
    696            )
    697          : null,
    698      // - otherwise try to get the "body" element
    699      () => walker.querySelector(rootNodeFront, "body"),
    700      // - finally get the documentElement element if nothing else worked.
    701      () => walker.documentElement(),
    702    ];
    703 
    704    // Try all default node selectors until a valid node is found.
    705    for (const selector of defaultNodeSelectors) {
    706      node = await selector();
    707      if (this.#pendingSelectionUnique !== pendingSelectionUnique) {
    708        // If this method was called again while waiting, bail out.
    709        return null;
    710      }
    711 
    712      if (node) {
    713        this.#defaultNode = node;
    714        return node;
    715      }
    716    }
    717 
    718    return null;
    719  }
    720 
    721  /**
    722   * Top level target front getter.
    723   */
    724  get currentTarget() {
    725    return this.commands.targetCommand.selectedTargetFront;
    726  }
    727 
    728  /**
    729   * Hooks the searchbar to show result and auto completion suggestions.
    730   */
    731  #setupSearchBox() {
    732    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
    733    this.searchClearButton = this.panelDoc.getElementById(
    734      "inspector-searchinput-clear"
    735    );
    736    this.searchResultsContainer = this.panelDoc.getElementById(
    737      "inspector-searchlabel-container"
    738    );
    739    this.searchNavigationContainer = this.panelDoc.getElementById(
    740      "inspector-searchnavigation-container"
    741    );
    742    this.searchPrevButton = this.panelDoc.getElementById(
    743      "inspector-searchnavigation-button-prev"
    744    );
    745    this.searchNextButton = this.panelDoc.getElementById(
    746      "inspector-searchnavigation-button-next"
    747    );
    748    this.searchResultsLabel = this.panelDoc.getElementById(
    749      "inspector-searchlabel"
    750    );
    751 
    752    this.searchResultsLabel.addEventListener("click", this.#onSearchLabelClick);
    753 
    754    this.searchBox.addEventListener("focus", this.#listenForSearchEvents, {
    755      once: true,
    756    });
    757  }
    758 
    759  #onSearchLabelClick = () => {
    760    // Focus on the search box as the search label
    761    // appears to be "inside" input
    762    this.searchBox.focus();
    763  };
    764 
    765  #listenForSearchEvents = () => {
    766    this.search.on("search-cleared", this.#clearSearchResultsLabel);
    767    this.search.on("search-result", this.#updateSearchResultsLabel);
    768  };
    769 
    770  #isFromInspectorWindow = event => {
    771    const win = event.originalTarget.ownerGlobal;
    772    return win === this.panelWin || win.parent === this.panelWin;
    773  };
    774 
    775  #createInspectorShortcuts = () => {
    776    this.inspectorShortcuts = new KeyShortcuts({
    777      window: this.panelDoc.defaultView,
    778      // The inspector search shortcuts need to be available from everywhere in the
    779      // inspector, and the inspector uses iframes (markupview, sidepanel webextensions).
    780      // Use the chromeEventHandler as the target to catch events from all frames.
    781      target: this.toolbox.getChromeEventHandler(),
    782    });
    783 
    784    const searchboxKey = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
    785    this.inspectorShortcuts.on(searchboxKey, event => {
    786      // Prevent overriding same shortcut from the computed/rule views
    787      if (
    788        event.originalTarget.closest("#sidebar-panel-ruleview") ||
    789        event.originalTarget.closest("#sidebar-panel-computedview") ||
    790        !this.#isFromInspectorWindow(event)
    791      ) {
    792        return;
    793      }
    794      event.preventDefault();
    795      this.searchBox.focus();
    796    });
    797    const eyedropperKey = INSPECTOR_L10N.getStr("inspector.eyedropper.key");
    798    this.inspectorShortcuts.on(eyedropperKey, event => {
    799      if (!this.#isFromInspectorWindow(event)) {
    800        return;
    801      }
    802      event.preventDefault();
    803      this.onEyeDropperButtonClicked();
    804    });
    805  };
    806 
    807  get searchSuggestions() {
    808    return this.search.autocompleter;
    809  }
    810 
    811  #clearSearchResultsLabel = result => {
    812    // Pipe the search-cleared event as this.search is a getter that will create
    813    // the InspectorSearch instance, which we don't really need/want when a callsite
    814    // only want to react to the search being cleared.
    815    this.emit("search-cleared");
    816    return this.#updateSearchResultsLabel(result, true);
    817  };
    818 
    819  #updateSearchResultsLabel = (result, clear = false) => {
    820    let str = "";
    821    if (!clear) {
    822      if (result) {
    823        str = INSPECTOR_L10N.getFormatStr(
    824          "inspector.searchResultsCount2",
    825          result.resultsIndex + 1,
    826          result.resultsLength
    827        );
    828        this.searchNavigationContainer.hidden = false;
    829      } else {
    830        str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
    831        this.searchNavigationContainer.hidden = true;
    832      }
    833 
    834      this.searchResultsContainer.hidden = false;
    835    } else {
    836      this.searchResultsContainer.hidden = true;
    837    }
    838 
    839    this.searchResultsLabel.textContent = str;
    840  };
    841 
    842  get React() {
    843    return this.#toolbox.React;
    844  }
    845 
    846  get ReactDOM() {
    847    return this.#toolbox.ReactDOM;
    848  }
    849 
    850  get ReactRedux() {
    851    return this.#toolbox.ReactRedux;
    852  }
    853 
    854  get browserRequire() {
    855    return this.#toolbox.browserRequire;
    856  }
    857 
    858  get InspectorTabPanel() {
    859    if (!this.#InspectorTabPanel) {
    860      this.#InspectorTabPanel = this.React.createFactory(
    861        this.browserRequire(
    862          "devtools/client/inspector/components/InspectorTabPanel"
    863        )
    864      );
    865    }
    866    return this.#InspectorTabPanel;
    867  }
    868 
    869  get InspectorSplitBox() {
    870    if (!this.#InspectorSplitBox) {
    871      this.#InspectorSplitBox = this.React.createFactory(
    872        this.browserRequire(
    873          "devtools/client/shared/components/splitter/SplitBox"
    874        )
    875      );
    876    }
    877    return this.#InspectorSplitBox;
    878  }
    879 
    880  get TabBar() {
    881    if (!this.#TabBar) {
    882      this.#TabBar = this.React.createFactory(
    883        this.browserRequire("devtools/client/shared/components/tabs/TabBar")
    884      );
    885    }
    886    return this.#TabBar;
    887  }
    888 
    889  /**
    890   * Check if the inspector should use the landscape mode.
    891   *
    892   * @return {boolean} true if the inspector should be in landscape mode.
    893   */
    894  #useLandscapeMode() {
    895    if (!this.panelDoc) {
    896      return true;
    897    }
    898 
    899    const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
    900    const width = splitterBox.clientWidth;
    901 
    902    return this.isThreePaneModeEnabled &&
    903      (this.toolbox.hostType == Toolbox.HostType.LEFT ||
    904        this.toolbox.hostType == Toolbox.HostType.RIGHT)
    905      ? width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD
    906      : width > PORTRAIT_MODE_WIDTH_THRESHOLD;
    907  }
    908 
    909  /**
    910   * Build Splitter located between the main and side area of
    911   * the Inspector panel.
    912   */
    913  #setupSplitter() {
    914    const { width, height, splitSidebarWidth } = this.getSidebarSize();
    915 
    916    this.sidebarSplitBoxRef = this.React.createRef();
    917 
    918    const splitter = this.InspectorSplitBox({
    919      className: "inspector-sidebar-splitter",
    920      initialWidth: width,
    921      initialHeight: height,
    922      minSize: "10%",
    923      maxSize: "80%",
    924      splitterSize: 1,
    925      endPanelControl: true,
    926      startPanel: this.InspectorTabPanel({
    927        id: "inspector-main-content",
    928      }),
    929      endPanel: this.InspectorSplitBox({
    930        initialWidth: splitSidebarWidth,
    931        minSize: "225px",
    932        maxSize: "80%",
    933        splitterSize: this.isThreePaneModeEnabled ? 1 : 0,
    934        endPanelControl: this.isThreePaneModeEnabled,
    935        startPanel: this.InspectorTabPanel({
    936          id: "inspector-rules-container",
    937        }),
    938        endPanel: this.InspectorTabPanel({
    939          id: "inspector-sidebar-container",
    940        }),
    941        ref: this.sidebarSplitBoxRef,
    942      }),
    943      vert: this.#useLandscapeMode(),
    944      onControlledPanelResized: this.onSidebarResized,
    945    });
    946 
    947    this.splitBox = this.ReactDOM.render(
    948      splitter,
    949      this.panelDoc.getElementById("inspector-splitter-box")
    950    );
    951 
    952    this.panelWin.addEventListener("resize", this.#onLazyPanelResize, true);
    953  }
    954 
    955  #onLazyPanelResize = debounce(
    956    () => {
    957      // We can be called on a closed window or destroyed toolbox because of the deferred task.
    958      if (
    959        this.panelWin?.closed ||
    960        this.#destroyed ||
    961        this.#toolbox.currentToolId !== "inspector"
    962      ) {
    963        return;
    964      }
    965 
    966      this.splitBox.setState({ vert: this.#useLandscapeMode() });
    967      this.emit("inspector-resize");
    968    },
    969    LAZY_RESIZE_INTERVAL_MS,
    970    this
    971  );
    972 
    973  getSidebarSize() {
    974    let width;
    975    let height;
    976    let splitSidebarWidth;
    977 
    978    // Initialize splitter size from preferences.
    979    try {
    980      width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
    981      height = Services.prefs.getIntPref(
    982        "devtools.toolsidebar-height.inspector"
    983      );
    984      splitSidebarWidth = Services.prefs.getIntPref(
    985        "devtools.toolsidebar-width.inspector.splitsidebar"
    986      );
    987    } catch (e) {
    988      // Set width and height of the splitter. Only one
    989      // value is really useful at a time depending on the current
    990      // orientation (vertical/horizontal).
    991      // Having both is supported by the splitter component.
    992      width = this.isThreePaneModeEnabled
    993        ? INITIAL_SIDEBAR_SIZE * 2
    994        : INITIAL_SIDEBAR_SIZE;
    995      height = INITIAL_SIDEBAR_SIZE;
    996      splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
    997    }
    998 
    999    return { width, height, splitSidebarWidth };
   1000  }
   1001 
   1002  onSidebarHidden() {
   1003    // Store the current splitter size to preferences.
   1004    const state = this.splitBox.state;
   1005    Services.prefs.setIntPref(
   1006      "devtools.toolsidebar-width.inspector",
   1007      state.width
   1008    );
   1009    Services.prefs.setIntPref(
   1010      "devtools.toolsidebar-height.inspector",
   1011      state.height
   1012    );
   1013    Services.prefs.setIntPref(
   1014      "devtools.toolsidebar-width.inspector.splitsidebar",
   1015      this.sidebarSplitBoxRef.current.state.width
   1016    );
   1017  }
   1018 
   1019  onSidebarResized(width, height) {
   1020    this.toolbox.emit("inspector-sidebar-resized", { width, height });
   1021  }
   1022 
   1023  /**
   1024   * Returns inspector tab that is active.
   1025   */
   1026  getActiveSidebar() {
   1027    return Services.prefs.getCharPref("devtools.inspector.activeSidebar");
   1028  }
   1029 
   1030  setActiveSidebar(toolId) {
   1031    Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
   1032  }
   1033 
   1034  /**
   1035   * Returns tab that is explicitly selected by user.
   1036   */
   1037  getSelectedSidebar() {
   1038    return Services.prefs.getCharPref("devtools.inspector.selectedSidebar");
   1039  }
   1040 
   1041  setSelectedSidebar(toolId) {
   1042    Services.prefs.setCharPref("devtools.inspector.selectedSidebar", toolId);
   1043  }
   1044 
   1045  onSidebarSelect(toolId) {
   1046    // Save the currently selected sidebar panel
   1047    this.setSelectedSidebar(toolId);
   1048    this.setActiveSidebar(toolId);
   1049 
   1050    // Then forces the panel creation by calling getPanel
   1051    // (This allows lazy loading the panels only once we select them)
   1052    this.getPanel(toolId);
   1053 
   1054    this.toolbox.emit("inspector-sidebar-select", toolId);
   1055  }
   1056 
   1057  onSidebarShown() {
   1058    const { width, height, splitSidebarWidth } = this.getSidebarSize();
   1059    this.splitBox.setState({ width, height });
   1060    this.sidebarSplitBoxRef.current.setState({ width: splitSidebarWidth });
   1061  }
   1062 
   1063  async onSidebarToggle() {
   1064    this.isThreePaneModeEnabled = !this.isThreePaneModeEnabled;
   1065    await this.#setupToolbar();
   1066    this.#addRuleView({ skipQueue: true });
   1067  }
   1068 
   1069  /**
   1070   * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
   1071   * split box, specifies the end panel control and resizes the split box width depending
   1072   * on the width of the toolbox.
   1073   */
   1074  #setSidebarSplitBoxState() {
   1075    const toolboxWidth = this.panelDoc.getElementById(
   1076      "inspector-splitter-box"
   1077    ).clientWidth;
   1078 
   1079    // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
   1080    // vertical mode) width.
   1081    const sidebarWidth = this.splitBox.state.width;
   1082    // This variable represents the width of the right panel in horizontal mode or
   1083    // bottom-right panel in vertical mode width in 3 pane mode.
   1084    let sidebarSplitboxWidth;
   1085 
   1086    if (this.#useLandscapeMode()) {
   1087      // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
   1088      // or bottom panel in vertical mode) width will be bigger than half of the
   1089      // toolbox's width.
   1090      const canDoubleSidebarWidth = sidebarWidth * 2 < toolboxWidth / 2;
   1091 
   1092      // Resize the main split box's end panel that contains the middle and right panel.
   1093      // Attempts to resize the main split box's end panel to be double the size of the
   1094      // existing sidebar's width when switching to 3 pane mode. However, if the middle
   1095      // and right panel's width together is greater than half of the toolbox's width,
   1096      // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
   1097      // the current toolbox's width.
   1098      this.splitBox.setState({
   1099        width: canDoubleSidebarWidth
   1100          ? sidebarWidth * 2
   1101          : (toolboxWidth * 2) / 3,
   1102      });
   1103 
   1104      // In landscape/horizontal mode, set the right panel back to its original
   1105      // inspector sidebar width if we can double the sidebar width. Otherwise, set
   1106      // the width of the right panel to be 1/3 of the toolbox's width since all 3
   1107      // panels will be equally sized.
   1108      sidebarSplitboxWidth = canDoubleSidebarWidth
   1109        ? sidebarWidth
   1110        : toolboxWidth / 3;
   1111    } else {
   1112      // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
   1113      // toolbox's width.
   1114      sidebarSplitboxWidth = toolboxWidth / 2;
   1115    }
   1116 
   1117    // Show the splitter inside the sidebar split box. Sets the width of the inspector
   1118    // sidebar and specify that the end (right in horizontal or bottom-right in
   1119    // vertical) panel of the sidebar split box should be controlled when resizing.
   1120    this.sidebarSplitBoxRef.current.setState({
   1121      endPanelControl: true,
   1122      splitterSize: 1,
   1123      width: sidebarSplitboxWidth,
   1124    });
   1125  }
   1126 
   1127  /**
   1128   * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
   1129   * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
   1130   * pane mode. Rule view is selected when switching to 2 pane mode. Selected sidebar pref
   1131   * is used otherwise.
   1132   */
   1133  #addRuleView({ skipQueue = false } = {}) {
   1134    const selectedSidebar = this.getSelectedSidebar();
   1135    const ruleViewSidebar = this.sidebarSplitBoxRef.current.startPanelContainer;
   1136 
   1137    if (this.isThreePaneModeEnabled) {
   1138      // Convert to 3 pane mode by removing the rule view from the inspector sidebar
   1139      // and adding the rule view to the middle (in landscape/horizontal mode) or
   1140      // bottom-left (in portrait/vertical mode) panel.
   1141      ruleViewSidebar.style.display = "block";
   1142 
   1143      this.#setSidebarSplitBoxState();
   1144 
   1145      // Force the rule view panel creation by calling getPanel
   1146      this.getPanel("ruleview");
   1147 
   1148      this.sidebar.removeTab("ruleview");
   1149      this.sidebar.select(selectedSidebar);
   1150 
   1151      this.ruleViewSideBar.addExistingTab(
   1152        "ruleview",
   1153        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
   1154        true
   1155      );
   1156 
   1157      this.ruleViewSideBar.show();
   1158    } else {
   1159      // When switching to 2 pane view, always set rule view as the active sidebar.
   1160      this.setActiveSidebar("ruleview");
   1161      // Removes the rule view from the 3 pane mode and adds the rule view to the main
   1162      // inspector sidebar.
   1163      ruleViewSidebar.style.display = "none";
   1164 
   1165      // Set the width of the split box (right panel in horziontal mode and bottom panel
   1166      // in vertical mode) to be the width of the inspector sidebar.
   1167      const splitterBox = this.panelDoc.getElementById(
   1168        "inspector-splitter-box"
   1169      );
   1170      this.splitBox.setState({
   1171        width: this.#useLandscapeMode()
   1172          ? this.sidebarSplitBoxRef.current.state.width
   1173          : splitterBox.clientWidth,
   1174      });
   1175 
   1176      // Hide the splitter to prevent any drag events in the sidebar split box and
   1177      // specify that the end (right panel in horziontal mode or bottom panel in vertical
   1178      // mode) panel should be uncontrolled when resizing.
   1179      this.sidebarSplitBoxRef.current.setState({
   1180        endPanelControl: false,
   1181        splitterSize: 0,
   1182      });
   1183 
   1184      this.ruleViewSideBar.hide();
   1185      this.ruleViewSideBar.removeTab("ruleview");
   1186 
   1187      if (skipQueue) {
   1188        this.sidebar.addExistingTab(
   1189          "ruleview",
   1190          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
   1191          true,
   1192          0
   1193        );
   1194      } else {
   1195        this.sidebar.queueExistingTab(
   1196          "ruleview",
   1197          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
   1198          true,
   1199          0
   1200        );
   1201      }
   1202    }
   1203 
   1204    // Adding or removing a tab from sidebar sets selectedSidebar by the active tab,
   1205    // which we should revert.
   1206    this.setSelectedSidebar(selectedSidebar);
   1207 
   1208    this.emit("ruleview-added");
   1209  }
   1210 
   1211  /**
   1212   * Returns a boolean indicating whether a sidebar panel instance exists.
   1213   */
   1214  hasPanel(id) {
   1215    return this.#panels.has(id);
   1216  }
   1217 
   1218  /**
   1219   * Lazily get and create panel instances displayed in the sidebar
   1220   */
   1221  getPanel(id) {
   1222    if (this.#panels.has(id)) {
   1223      return this.#panels.get(id);
   1224    }
   1225 
   1226    let panel;
   1227    switch (id) {
   1228      case "animationinspector": {
   1229        const AnimationInspector = this.browserRequire(
   1230          "devtools/client/inspector/animation/animation"
   1231        );
   1232        panel = new AnimationInspector(this, this.panelWin);
   1233        break;
   1234      }
   1235      case "boxmodel": {
   1236        // box-model isn't a panel on its own, it used to, now it is being used by
   1237        // the layout view which retrieves an instance via getPanel.
   1238        const BoxModel = require("resource://devtools/client/inspector/boxmodel/box-model.js");
   1239        panel = new BoxModel(this, this.panelWin);
   1240        break;
   1241      }
   1242      case "changesview": {
   1243        const ChangesView = this.browserRequire(
   1244          "devtools/client/inspector/changes/ChangesView"
   1245        );
   1246        panel = new ChangesView(this, this.panelWin);
   1247        break;
   1248      }
   1249      case "compatibilityview": {
   1250        const CompatibilityView = this.browserRequire(
   1251          "devtools/client/inspector/compatibility/CompatibilityView"
   1252        );
   1253        panel = new CompatibilityView(this, this.panelWin);
   1254        break;
   1255      }
   1256      case "computedview": {
   1257        const { ComputedViewTool } = this.browserRequire(
   1258          "devtools/client/inspector/computed/computed"
   1259        );
   1260        panel = new ComputedViewTool(this, this.panelWin);
   1261        break;
   1262      }
   1263      case "fontinspector": {
   1264        const FontInspector = this.browserRequire(
   1265          "devtools/client/inspector/fonts/fonts"
   1266        );
   1267        panel = new FontInspector(this, this.panelWin);
   1268        break;
   1269      }
   1270      case "layoutview": {
   1271        const LayoutView = this.browserRequire(
   1272          "devtools/client/inspector/layout/layout"
   1273        );
   1274        panel = new LayoutView(this, this.panelWin);
   1275        break;
   1276      }
   1277      case "ruleview": {
   1278        const {
   1279          RuleViewTool,
   1280        } = require("resource://devtools/client/inspector/rules/rules.js");
   1281        panel = new RuleViewTool(this, this.panelWin);
   1282        break;
   1283      }
   1284      default:
   1285        // This is a custom panel or a non lazy-loaded one.
   1286        return null;
   1287    }
   1288 
   1289    if (panel) {
   1290      this.#panels.set(id, panel);
   1291    }
   1292 
   1293    return panel;
   1294  }
   1295 
   1296  /**
   1297   * Build the sidebar.
   1298   */
   1299  #setupSidebar() {
   1300    const sidebar = this.panelDoc.getElementById("inspector-sidebar");
   1301    const options = {
   1302      showAllTabsMenu: true,
   1303      allTabsMenuButtonTooltip: INSPECTOR_L10N.getStr(
   1304        "allTabsMenuButton.tooltip"
   1305      ),
   1306      sidebarToggleButton: {
   1307        collapsed: !this.isThreePaneModeEnabled,
   1308        collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
   1309        expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
   1310        onClick: this.onSidebarToggle,
   1311      },
   1312    };
   1313 
   1314    this.sidebar = new ToolSidebar(sidebar, this, options);
   1315    this.sidebar.on("select", this.onSidebarSelect);
   1316 
   1317    const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
   1318    this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, {
   1319      hideTabstripe: true,
   1320    });
   1321 
   1322    // Append all side panels
   1323    this.#addRuleView();
   1324 
   1325    // Inspector sidebar panels in order of appearance.
   1326    const sidebarPanels = [];
   1327    sidebarPanels.push({
   1328      id: "layoutview",
   1329      title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
   1330    });
   1331 
   1332    sidebarPanels.push({
   1333      id: "computedview",
   1334      title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
   1335    });
   1336 
   1337    sidebarPanels.push({
   1338      id: "changesview",
   1339      title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
   1340    });
   1341 
   1342    sidebarPanels.push({
   1343      id: "compatibilityview",
   1344      title: INSPECTOR_L10N.getStr("inspector.sidebar.compatibilityViewTitle"),
   1345    });
   1346 
   1347    sidebarPanels.push({
   1348      id: "fontinspector",
   1349      title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
   1350    });
   1351 
   1352    sidebarPanels.push({
   1353      id: "animationinspector",
   1354      title: INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
   1355    });
   1356 
   1357    const defaultTab = this.getActiveSidebar();
   1358 
   1359    for (const { id, title } of sidebarPanels) {
   1360      // The Computed panel is not a React-based panel. We pick its element container from
   1361      // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
   1362      // other panels when using the Inspector's tool sidebar.
   1363      if (id === "computedview") {
   1364        this.sidebar.queueExistingTab(id, title, defaultTab === id);
   1365      } else {
   1366        // When `panel` is a function, it is called when the tab should render. It is
   1367        // expected to return a React component to populate the tab's content area.
   1368        // Calling this method on-demand allows us to lazy-load the requested panel.
   1369        this.sidebar.queueTab(
   1370          id,
   1371          title,
   1372          {
   1373            props: {
   1374              id,
   1375              title,
   1376            },
   1377            panel: () => {
   1378              return this.getPanel(id).provider;
   1379            },
   1380          },
   1381          defaultTab === id
   1382        );
   1383      }
   1384    }
   1385 
   1386    this.sidebar.addAllQueuedTabs();
   1387 
   1388    // Persist splitter state in preferences.
   1389    this.sidebar.on("show", this.onSidebarShown);
   1390    this.sidebar.on("hide", this.onSidebarHidden);
   1391    this.sidebar.on("destroy", this.onSidebarHidden);
   1392 
   1393    this.sidebar.show();
   1394  }
   1395 
   1396  /**
   1397   * Setup any extension sidebar already registered to the toolbox when the inspector.
   1398   * has been created for the first time.
   1399   */
   1400  #setupExtensionSidebars() {
   1401    for (const [sidebarId, { title }] of this.toolbox
   1402      .inspectorExtensionSidebars) {
   1403      this.addExtensionSidebar(sidebarId, { title });
   1404    }
   1405  }
   1406 
   1407  /**
   1408   * Create a side-panel tab controlled by an extension
   1409   * using the devtools.panels.elements.createSidebarPane and sidebar object API
   1410   *
   1411   * @param {string} id
   1412   *        An unique id for the sidebar tab.
   1413   * @param {object} options
   1414   * @param {string} options.title
   1415   *        The tab title
   1416   */
   1417  addExtensionSidebar(id, { title }) {
   1418    if (this.#panels.has(id)) {
   1419      throw new Error(
   1420        `Cannot create an extension sidebar for the existent id: ${id}`
   1421      );
   1422    }
   1423 
   1424    // Load the ExtensionSidebar component via the Browser Loader as it ultimately loads Reps and Object Inspector,
   1425    // which are expected to be loaded in a document scope.
   1426    const ExtensionSidebar = this.browserRequire(
   1427      "resource://devtools/client/inspector/extensions/extension-sidebar.js"
   1428    );
   1429    const extensionSidebar = new ExtensionSidebar(this, { id, title });
   1430 
   1431    // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
   1432    // the render of the extension title (e.g. use the icon in the sidebar and show the
   1433    // extension name in a tooltip).
   1434    this.addSidebarTab(id, title, extensionSidebar.provider, false);
   1435 
   1436    this.#panels.set(id, extensionSidebar);
   1437 
   1438    // Emit the created ExtensionSidebar instance to the listeners registered
   1439    // on the toolbox by the "devtools.panels.elements" WebExtensions API.
   1440    this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
   1441  }
   1442 
   1443  /**
   1444   * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
   1445   * extension has been disable/uninstalled while the toolbox and inspector were
   1446   * still open).
   1447   *
   1448   * @param {string} id
   1449   *        The id of the sidebar tab to destroy.
   1450   */
   1451  removeExtensionSidebar(id) {
   1452    if (!this.#panels.has(id)) {
   1453      throw new Error(`Unable to find a sidebar panel with id "${id}"`);
   1454    }
   1455 
   1456    const panel = this.#panels.get(id);
   1457 
   1458    const ExtensionSidebar = this.browserRequire(
   1459      "resource://devtools/client/inspector/extensions/extension-sidebar.js"
   1460    );
   1461    if (!(panel instanceof ExtensionSidebar)) {
   1462      throw new Error(
   1463        `The sidebar panel with id "${id}" is not an ExtensionSidebar`
   1464      );
   1465    }
   1466 
   1467    this.#panels.delete(id);
   1468    this.sidebar.removeTab(id);
   1469    panel.destroy();
   1470  }
   1471 
   1472  /**
   1473   * Register a side-panel tab. This API can be used outside of
   1474   * DevTools (e.g. from an extension) as well as by DevTools
   1475   * code base.
   1476   *
   1477   * @param {string} tab uniq id
   1478   * @param {string} title tab title
   1479   * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
   1480   * @param {boolean} selected true if the panel should be selected
   1481   */
   1482  addSidebarTab(id, title, panel, selected) {
   1483    this.sidebar.addTab(id, title, panel, selected);
   1484  }
   1485 
   1486  /**
   1487   * Method to check whether the document is a HTML document and
   1488   * pickColorFromPage method is available or not.
   1489   *
   1490   * @return {boolean} true if the eyedropper highlighter is supported by the current
   1491   *         document.
   1492   */
   1493  async supportsEyeDropper() {
   1494    try {
   1495      return await this.inspectorFront.supportsHighlighters();
   1496    } catch (e) {
   1497      console.error(e);
   1498      return false;
   1499    }
   1500  }
   1501 
   1502  async #setupToolbar() {
   1503    this.#teardownToolbar();
   1504 
   1505    // Setup the add-node button.
   1506    this.addNodeButton = this.panelDoc.getElementById(
   1507      "inspector-element-add-button"
   1508    );
   1509    this.addNodeButton.addEventListener("click", this.addNode);
   1510 
   1511    // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
   1512    const canShowEyeDropper = await this.supportsEyeDropper();
   1513 
   1514    // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
   1515    // available.
   1516    if (!this.panelDoc) {
   1517      return;
   1518    }
   1519 
   1520    if (canShowEyeDropper) {
   1521      this.eyeDropperButton = this.panelDoc.getElementById(
   1522        "inspector-eyedropper-toggle"
   1523      );
   1524      this.eyeDropperButton.disabled = false;
   1525      const shortcutKey = INSPECTOR_L10N.getStr(
   1526        "inspector.eyedropper.key"
   1527      ).replace("CmdOrCtrl", osString == "Darwin" ? "Cmd" : "Ctrl");
   1528 
   1529      this.eyeDropperButton.title = INSPECTOR_L10N.getFormatStr(
   1530        "inspector.eyedropper.label2",
   1531        shortcutKey
   1532      );
   1533      this.eyeDropperButton.addEventListener(
   1534        "click",
   1535        this.onEyeDropperButtonClicked
   1536      );
   1537    } else {
   1538      const eyeDropperButton = this.panelDoc.getElementById(
   1539        "inspector-eyedropper-toggle"
   1540      );
   1541      eyeDropperButton.disabled = true;
   1542      eyeDropperButton.title = INSPECTOR_L10N.getStr(
   1543        "eyedropper.disabled.title"
   1544      );
   1545    }
   1546 
   1547    this.emit("inspector-toolbar-updated");
   1548  }
   1549 
   1550  #teardownToolbar() {
   1551    if (this.addNodeButton) {
   1552      this.addNodeButton.removeEventListener("click", this.addNode);
   1553      this.addNodeButton = null;
   1554    }
   1555 
   1556    if (this.eyeDropperButton) {
   1557      this.eyeDropperButton.removeEventListener(
   1558        "click",
   1559        this.onEyeDropperButtonClicked
   1560      );
   1561      this.eyeDropperButton = null;
   1562    }
   1563  }
   1564 
   1565  #selectionCssSelectors = null;
   1566 
   1567  /**
   1568   * Set the array of CSS selectors for the currently selected node.
   1569   * We use an array of selectors in case the element is in iframes.
   1570   * Will store the current target url along with it to allow pre-selection at
   1571   * reload
   1572   */
   1573  set selectionCssSelectors(cssSelectors = []) {
   1574    if (this.#destroyed) {
   1575      return;
   1576    }
   1577 
   1578    this.#selectionCssSelectors = {
   1579      selectors: cssSelectors,
   1580      url: this.currentTarget.url,
   1581    };
   1582  }
   1583 
   1584  /**
   1585   * Get the CSS selectors for the current selection if any, that is, if a node
   1586   * is actually selected and that node has been selected while on the same url
   1587   */
   1588  get selectionCssSelectors() {
   1589    if (
   1590      this.#selectionCssSelectors &&
   1591      this.#selectionCssSelectors.url === this.currentTarget.url
   1592    ) {
   1593      return this.#selectionCssSelectors.selectors;
   1594    }
   1595    return [];
   1596  }
   1597 
   1598  /**
   1599   * On any new selection made by the user, store the array of css selectors
   1600   * of the selected node so it can be restored after reload of the same page
   1601   */
   1602  #updateSelectionCssSelectors() {
   1603    if (!this.selection.isElementNode()) {
   1604      return;
   1605    }
   1606 
   1607    this.commands.inspectorCommand
   1608      .getNodeFrontSelectorsFromTopDocument(this.selection.nodeFront)
   1609      .then(selectors => {
   1610        this.selectionCssSelectors = selectors;
   1611        // emit an event so tests relying on the property being set can properly wait
   1612        // for it.
   1613        this.emitForTests("selection-css-selectors-updated", selectors);
   1614      }, this.#handleRejectionIfNotDestroyed);
   1615  }
   1616 
   1617  /**
   1618   * Can a new HTML element be inserted into the currently selected element?
   1619   *
   1620   * @return {boolean}
   1621   */
   1622  canAddHTMLChild() {
   1623    const selection = this.selection;
   1624 
   1625    // Don't allow to insert an element into these elements. This should only
   1626    // contain elements where walker.insertAdjacentHTML has no effect.
   1627    const invalidTagNames = ["html", "iframe"];
   1628 
   1629    return (
   1630      selection.isHTMLNode() &&
   1631      selection.isElementNode() &&
   1632      !selection.isPseudoElementNode() &&
   1633      !selection.isNativeAnonymousNode() &&
   1634      !invalidTagNames.includes(selection.nodeFront.nodeName.toLowerCase())
   1635    );
   1636  }
   1637 
   1638  /**
   1639   * Update the state of the add button in the toolbar depending on the current selection.
   1640   */
   1641  #updateAddElementButton() {
   1642    const btn = this.panelDoc.getElementById("inspector-element-add-button");
   1643    if (this.canAddHTMLChild()) {
   1644      btn.removeAttribute("disabled");
   1645    } else {
   1646      btn.setAttribute("disabled", "true");
   1647    }
   1648  }
   1649 
   1650  /**
   1651   * Handler for the "host-changed" event from the toolbox. Resets the inspector
   1652   * sidebar sizes when the toolbox host type changes.
   1653   */
   1654  #onHostChanged = async () => {
   1655    // Eagerly call our resize handling code to process the fact that we
   1656    // switched hosts. If we don't do this, we'll wait for resize events + 200ms
   1657    // to have passed, which causes the old layout to noticeably show up in the
   1658    // new host, followed by the updated one.
   1659    await this.#onLazyPanelResize();
   1660    // Note that we may have been destroyed by now, especially in tests, so we
   1661    // need to check if that's happened before touching anything else.
   1662    if (!this.currentTarget || !this.isThreePaneModeEnabled) {
   1663      return;
   1664    }
   1665 
   1666    // When changing hosts, the toolbox chromeEventHandler might change, for instance when
   1667    // switching from docked to window hosts. Recreate the inspector shortcuts.
   1668    this.inspectorShortcuts.destroy();
   1669    this.#createInspectorShortcuts();
   1670    this.#setSidebarSplitBoxState();
   1671  };
   1672 
   1673  /**
   1674   * When a new node is selected.
   1675   */
   1676  #onNewSelection = (value, reason) => {
   1677    if (reason === "selection-destroy") {
   1678      return;
   1679    }
   1680 
   1681    this.#updateAddElementButton();
   1682    this.#updateSelectionCssSelectors();
   1683 
   1684    const selfUpdate = this.updating("inspector-panel");
   1685    executeSoon(() => {
   1686      try {
   1687        selfUpdate(this.selection.nodeFront);
   1688        Glean.devtoolsInspector.nodeSelectionCount.add(1);
   1689      } catch (ex) {
   1690        console.error(ex);
   1691      }
   1692    });
   1693  };
   1694 
   1695  /**
   1696   * Delay the "inspector-updated" notification while a tool
   1697   * is updating itself.  Returns a function that must be
   1698   * invoked when the tool is done updating with the node
   1699   * that the tool is viewing.
   1700   */
   1701  updating(name) {
   1702    if (
   1703      this.#updateProgress &&
   1704      this.#updateProgress.node != this.selection.nodeFront
   1705    ) {
   1706      this.#cancelUpdate();
   1707    }
   1708 
   1709    if (!this.#updateProgress) {
   1710      // Start an update in progress.
   1711      const self = this;
   1712      this.#updateProgress = {
   1713        node: this.selection.nodeFront,
   1714        outstanding: new Set(),
   1715        checkDone() {
   1716          if (this !== self.#updateProgress) {
   1717            return;
   1718          }
   1719          // Cancel update if there is no `selection` anymore.
   1720          // It can happen if the inspector panel is already destroyed.
   1721          if (!self.selection || this.node !== self.selection.nodeFront) {
   1722            self.#cancelUpdate();
   1723            return;
   1724          }
   1725          if (this.outstanding.size !== 0) {
   1726            return;
   1727          }
   1728 
   1729          self.#updateProgress = null;
   1730          self.emit("inspector-updated", name);
   1731        },
   1732      };
   1733    }
   1734 
   1735    const progress = this.#updateProgress;
   1736    const done = function () {
   1737      progress.outstanding.delete(done);
   1738      progress.checkDone();
   1739    };
   1740    progress.outstanding.add(done);
   1741    return done;
   1742  }
   1743 
   1744  /**
   1745   * Cancel notification of inspector updates.
   1746   */
   1747  #cancelUpdate() {
   1748    this.#updateProgress = null;
   1749  }
   1750 
   1751  /**
   1752   * When a node is deleted, select its parent node or the defaultNode if no
   1753   * parent is found (may happen when deleting an iframe inside which the
   1754   * node was selected).
   1755   */
   1756  #onDetached = parentNode => {
   1757    this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
   1758    const nodeFront = parentNode ? parentNode : this.#defaultNode;
   1759    this.selection.setNodeFront(nodeFront, { reason: "detached" });
   1760  };
   1761 
   1762  /**
   1763   * Destroy the inspector.
   1764   */
   1765  destroy() {
   1766    if (this.#destroyed) {
   1767      return;
   1768    }
   1769    this.#destroyed = true;
   1770 
   1771    // Prevents any further action from being dispatched
   1772    this.store.dispatch(START_IGNORE_ACTION);
   1773 
   1774    this.#cancelUpdate();
   1775 
   1776    this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
   1777    this.selection.off("new-node-front", this.#onNewSelection);
   1778    this.selection.off("detached-front", this.#onDetached);
   1779    this.toolbox.nodePicker.off("picker-node-canceled", this.onPickerCanceled);
   1780    this.toolbox.nodePicker.off("picker-node-hovered", this.onPickerHovered);
   1781    this.toolbox.nodePicker.off("picker-node-picked", this.onPickerPicked);
   1782 
   1783    // Destroy the sidebar first as it may unregister stuff
   1784    // and still use random attributes on inspector and layout panel
   1785    this.sidebar.destroy();
   1786    // Unregister sidebar listener *after* destroying it
   1787    // in order to process its destroy event and save sidebar sizes
   1788    this.sidebar.off("select", this.onSidebarSelect);
   1789    this.sidebar.off("show", this.onSidebarShown);
   1790    this.sidebar.off("hide", this.onSidebarHidden);
   1791    this.sidebar.off("destroy", this.onSidebarHidden);
   1792 
   1793    for (const [, panel] of this.#panels) {
   1794      panel.destroy();
   1795    }
   1796    this.#panels.clear();
   1797 
   1798    if (this.#highlighters) {
   1799      this.#highlighters.destroy();
   1800    }
   1801 
   1802    if (this.#search) {
   1803      this.#search.destroy();
   1804      this.#search = null;
   1805    }
   1806 
   1807    this.ruleViewSideBar.destroy();
   1808    this.ruleViewSideBar = null;
   1809 
   1810    this.#destroyMarkup();
   1811 
   1812    this.#teardownToolbar();
   1813 
   1814    this.prefObserver.on(
   1815      DEFAULT_COLOR_UNIT_PREF,
   1816      this.#handleDefaultColorUnitPrefChange
   1817    );
   1818    this.prefObserver.destroy();
   1819 
   1820    this.breadcrumbs.destroy();
   1821    this.styleChangeTracker.destroy();
   1822    this.inspectorShortcuts.destroy();
   1823    this.inspectorShortcuts = null;
   1824 
   1825    this.commands.targetCommand.unwatchTargets({
   1826      types: [this.commands.targetCommand.TYPES.FRAME],
   1827      onAvailable: this.#onTargetAvailable,
   1828      onSelected: this.#onTargetSelected,
   1829      onDestroyed: this.#onTargetDestroyed,
   1830    });
   1831    const { resourceCommand } = this.commands;
   1832    resourceCommand.unwatchResources(this.#watchedResources, {
   1833      onAvailable: this.onResourceAvailable,
   1834    });
   1835 
   1836    this.#InspectorTabPanel = null;
   1837    this.#TabBar = null;
   1838    this.#InspectorSplitBox = null;
   1839    this.sidebarSplitBoxRef = null;
   1840    // Note that we do not unmount inspector-splitter-box
   1841    // as it regresses inspector closing performance while not releasing
   1842    // any object (bug 1729925)
   1843    this.splitBox = null;
   1844 
   1845    this.#isThreePaneModeEnabled = null;
   1846    this.#markupBox = null;
   1847    this.#markupFrame = null;
   1848    this.#toolbox = null;
   1849    this.#commands = null;
   1850    this.breadcrumbs = null;
   1851    this.inspectorFront = null;
   1852    this.#cssProperties = null;
   1853    this.accessibilityFront = null;
   1854    this.#highlighters = null;
   1855    this.walker = null;
   1856    this.#defaultNode = null;
   1857    this.panelDoc = null;
   1858    this.panelWin.inspector = null;
   1859    this.panelWin = null;
   1860    this.resultsLength = null;
   1861    this.searchBox.removeEventListener("focus", this.#listenForSearchEvents);
   1862    this.searchBox = null;
   1863    this.show3PaneTooltip = null;
   1864    this.sidebar = null;
   1865    this.store = null;
   1866    this.telemetry = null;
   1867    this.searchResultsLabel.removeEventListener(
   1868      "click",
   1869      this.#onSearchLabelClick
   1870    );
   1871    this.searchResultsLabel = null;
   1872  }
   1873 
   1874  #destroyMarkup() {
   1875    if (this.markup) {
   1876      this.markup.destroy();
   1877      this.markup = null;
   1878    }
   1879 
   1880    if (this.#markupBox) {
   1881      this.#markupBox.style.visibility = "hidden";
   1882    }
   1883  }
   1884 
   1885  onEyeDropperButtonClicked() {
   1886    this.eyeDropperButton.classList.contains("checked")
   1887      ? this.hideEyeDropper()
   1888      : this.showEyeDropper();
   1889  }
   1890 
   1891  startEyeDropperListeners() {
   1892    this.toolbox.tellRDMAboutPickerState(true, PICKER_TYPES.EYEDROPPER);
   1893    this.inspectorFront.once("color-pick-canceled", this.onEyeDropperDone);
   1894    this.inspectorFront.once("color-picked", this.onEyeDropperDone);
   1895    this.once("new-root", this.onEyeDropperDone);
   1896  }
   1897 
   1898  stopEyeDropperListeners() {
   1899    this.toolbox
   1900      .tellRDMAboutPickerState(false, PICKER_TYPES.EYEDROPPER)
   1901      .catch(console.error);
   1902    this.inspectorFront.off("color-pick-canceled", this.onEyeDropperDone);
   1903    this.inspectorFront.off("color-picked", this.onEyeDropperDone);
   1904    this.off("new-root", this.onEyeDropperDone);
   1905  }
   1906 
   1907  onEyeDropperDone() {
   1908    this.eyeDropperButton.classList.remove("checked");
   1909    this.stopEyeDropperListeners();
   1910    this.panelWin.focus();
   1911  }
   1912 
   1913  /**
   1914   * Show the eyedropper on the page.
   1915   *
   1916   * @return {Promise} resolves when the eyedropper is visible.
   1917   */
   1918  showEyeDropper() {
   1919    // The eyedropper button doesn't exist, most probably because the actor doesn't
   1920    // support the pickColorFromPage, or because the page isn't HTML.
   1921    if (!this.eyeDropperButton) {
   1922      return null;
   1923    }
   1924    // turn off node picker when color picker is starting
   1925    this.toolbox.nodePicker.stop({ canceled: true }).catch(console.error);
   1926    this.eyeDropperButton.classList.add("checked");
   1927    this.startEyeDropperListeners();
   1928    return this.inspectorFront
   1929      .pickColorFromPage({ copyOnSelect: true })
   1930      .catch(console.error);
   1931  }
   1932 
   1933  /**
   1934   * Hide the eyedropper.
   1935   *
   1936   * @return {Promise} resolves when the eyedropper is hidden.
   1937   */
   1938  hideEyeDropper() {
   1939    // The eyedropper button doesn't exist, most probably  because the page isn't HTML.
   1940    if (!this.eyeDropperButton) {
   1941      return null;
   1942    }
   1943 
   1944    this.eyeDropperButton.classList.remove("checked");
   1945    this.stopEyeDropperListeners();
   1946    return this.inspectorFront.cancelPickColorFromPage().catch(console.error);
   1947  }
   1948 
   1949  /**
   1950   * Create a new node as the last child of the current selection, expand the
   1951   * parent and select the new node.
   1952   */
   1953  async addNode() {
   1954    if (!this.canAddHTMLChild()) {
   1955      return;
   1956    }
   1957 
   1958    // turn off node picker when add node is triggered
   1959    this.toolbox.nodePicker.stop({ canceled: true });
   1960 
   1961    // turn off color picker when add node is triggered
   1962    this.hideEyeDropper();
   1963 
   1964    const nodeFront = this.selection.nodeFront;
   1965    const html = "<div></div>";
   1966 
   1967    // Insert the html and expect a childList markup mutation.
   1968    const onMutations = this.once("markupmutation");
   1969    await nodeFront.walkerFront.insertAdjacentHTML(
   1970      this.selection.nodeFront,
   1971      "beforeEnd",
   1972      html
   1973    );
   1974    await onMutations;
   1975 
   1976    // Expand the parent node.
   1977    this.markup.expandNode(nodeFront);
   1978  }
   1979 
   1980  /**
   1981   * Toggle a pseudo class.
   1982   */
   1983  togglePseudoClass(pseudo) {
   1984    if (this.selection.isElementNode()) {
   1985      const node = this.selection.nodeFront;
   1986      if (node.hasPseudoClassLock(pseudo)) {
   1987        return node.walkerFront.removePseudoClassLock(node, pseudo, {
   1988          parents: true,
   1989        });
   1990      }
   1991 
   1992      const hierarchical = pseudo == ":hover" || pseudo == ":active";
   1993      return node.walkerFront.addPseudoClassLock(node, pseudo, {
   1994        parents: hierarchical,
   1995      });
   1996    }
   1997    return Promise.resolve();
   1998  }
   1999 
   2000  /**
   2001   * Returns true if the "Change pseudo class" (either via the ":hov" panel checkboxes,
   2002   * or the markup view context menu entries) can be performed for the currently selected node.
   2003   *
   2004   * @returns {boolean}
   2005   */
   2006  canTogglePseudoClassForSelectedNode() {
   2007    if (!this.selection) {
   2008      return false;
   2009    }
   2010 
   2011    return (
   2012      this.selection.isElementNode() && !this.selection.isPseudoElementNode()
   2013    );
   2014  }
   2015 
   2016  /**
   2017   * Initiate screenshot command on selected node.
   2018   */
   2019  async screenshotNode() {
   2020    // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
   2021    // is still visible, therefore showing it in the picture.
   2022    // Note that other highlighters will still be visible. See Bug 1663881
   2023    await this.highlighters.hideHighlighterType(
   2024      this.highlighters.TYPES.BOXMODEL
   2025    );
   2026 
   2027    const clipboardEnabled = Services.prefs.getBoolPref(
   2028      "devtools.screenshot.clipboard.enabled"
   2029    );
   2030    const args = {
   2031      file: !clipboardEnabled,
   2032      nodeActorID: this.selection.nodeFront.actorID,
   2033      clipboard: clipboardEnabled,
   2034    };
   2035 
   2036    const messages = await captureAndSaveScreenshot(
   2037      this.selection.nodeFront.targetFront,
   2038      this.panelWin,
   2039      args
   2040    );
   2041    const notificationBox = this.toolbox.getNotificationBox();
   2042    const priorityMap = {
   2043      error: notificationBox.PRIORITY_CRITICAL_HIGH,
   2044      warn: notificationBox.PRIORITY_WARNING_HIGH,
   2045    };
   2046    for (const { text, level } of messages) {
   2047      // captureAndSaveScreenshot returns "saved" messages, that indicate where the
   2048      // screenshot was saved. We don't want to display them as the download UI can be
   2049      // used to open the file.
   2050      if (level !== "warn" && level !== "error") {
   2051        continue;
   2052      }
   2053      notificationBox.appendNotification(text, null, null, priorityMap[level]);
   2054    }
   2055  }
   2056 
   2057  /**
   2058   * Returns an object containing the shared handler functions used in React components.
   2059   */
   2060  getCommonComponentProps() {
   2061    return {
   2062      setSelectedNode: this.selection.setNodeFront,
   2063    };
   2064  }
   2065 
   2066  onPickerCanceled() {
   2067    this.highlighters.hideHighlighterType(this.highlighters.TYPES.BOXMODEL);
   2068  }
   2069 
   2070  onPickerHovered(nodeFront) {
   2071    this.highlighters.showHighlighterTypeForNode(
   2072      this.highlighters.TYPES.BOXMODEL,
   2073      nodeFront
   2074    );
   2075  }
   2076 
   2077  onPickerPicked(nodeFront) {
   2078    if (this.toolbox.isDebugTargetFenix()) {
   2079      // When debugging a phone, as we don't have the "hover overlay", we want to provide
   2080      // feedback to the user so they know where they tapped
   2081      this.highlighters.showHighlighterTypeForNode(
   2082        this.highlighters.TYPES.BOXMODEL,
   2083        nodeFront,
   2084        { duration: this.HIGHLIGHTER_AUTOHIDE_TIMER }
   2085      );
   2086      return;
   2087    }
   2088    this.highlighters.hideHighlighterType(this.highlighters.TYPES.BOXMODEL);
   2089  }
   2090 
   2091  async inspectNodeActor(nodeGrip, reason) {
   2092    const nodeFront =
   2093      await this.inspectorFront.getNodeFrontFromNodeGrip(nodeGrip);
   2094    if (!nodeFront) {
   2095      console.error(
   2096        "The object cannot be linked to the inspector, the " +
   2097          "corresponding nodeFront could not be found."
   2098      );
   2099      return false;
   2100    }
   2101 
   2102    const isAttached = await this.walker.isInDOMTree(nodeFront);
   2103    if (!isAttached) {
   2104      console.error("Selected DOMNode is not attached to the document tree.");
   2105      return false;
   2106    }
   2107 
   2108    await this.selection.setNodeFront(nodeFront, { reason });
   2109    return true;
   2110  }
   2111 
   2112  /**
   2113   * Called by toolbox.js on `Esc` keydown.
   2114   *
   2115   * @param {AbortController} abortController
   2116   */
   2117  onToolboxChromeEventHandlerEscapeKeyDown(abortController) {
   2118    // If the event tooltip is displayed, hide it and prevent the Esc event listener
   2119    // of the toolbox to occur (e.g. don't toggle split console)
   2120    if (
   2121      this.markup.hasEventDetailsTooltip() &&
   2122      this.markup.eventDetailsTooltip.isVisible()
   2123    ) {
   2124      this.markup.eventDetailsTooltip.hide();
   2125      abortController.abort();
   2126    }
   2127  }
   2128 }
   2129 
   2130 exports.Inspector = Inspector;