forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1972 lines
		
	
	
	
		
			61 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1972 lines
		
	
	
	
		
			61 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const Services = require("Services");
 | 
						|
const promise = require("promise");
 | 
						|
const EventEmitter = require("devtools/shared/event-emitter");
 | 
						|
const flags = require("devtools/shared/flags");
 | 
						|
const { executeSoon } = require("devtools/shared/DevToolsUtils");
 | 
						|
const { Toolbox } = require("devtools/client/framework/toolbox");
 | 
						|
const createStore = require("devtools/client/inspector/store");
 | 
						|
const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
 | 
						|
 | 
						|
// Use privileged promise in panel documents to prevent having them to freeze
 | 
						|
// during toolbox destruction. See bug 1402779.
 | 
						|
const Promise = require("Promise");
 | 
						|
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "HTMLBreadcrumbs",
 | 
						|
  "devtools/client/inspector/breadcrumbs",
 | 
						|
  true
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "KeyShortcuts",
 | 
						|
  "devtools/client/shared/key-shortcuts"
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "InspectorSearch",
 | 
						|
  "devtools/client/inspector/inspector-search",
 | 
						|
  true
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "ToolSidebar",
 | 
						|
  "devtools/client/inspector/toolsidebar",
 | 
						|
  true
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "MarkupView",
 | 
						|
  "devtools/client/inspector/markup/markup"
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "HighlightersOverlay",
 | 
						|
  "devtools/client/inspector/shared/highlighters-overlay"
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "ExtensionSidebar",
 | 
						|
  "devtools/client/inspector/extensions/extension-sidebar"
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "PICKER_TYPES",
 | 
						|
  "devtools/shared/picker-constants"
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "captureAndSaveScreenshot",
 | 
						|
  "devtools/client/shared/screenshot",
 | 
						|
  true
 | 
						|
);
 | 
						|
 | 
						|
loader.lazyImporter(
 | 
						|
  this,
 | 
						|
  "DeferredTask",
 | 
						|
  "resource://gre/modules/DeferredTask.jsm"
 | 
						|
);
 | 
						|
 | 
						|
const { LocalizationHelper, localizeMarkup } = require("devtools/shared/l10n");
 | 
						|
const INSPECTOR_L10N = new LocalizationHelper(
 | 
						|
  "devtools/client/locales/inspector.properties"
 | 
						|
);
 | 
						|
const {
 | 
						|
  FluentL10n,
 | 
						|
} = require("devtools/client/shared/fluent-l10n/fluent-l10n");
 | 
						|
 | 
						|
// Sidebar dimensions
 | 
						|
const INITIAL_SIDEBAR_SIZE = 350;
 | 
						|
 | 
						|
// How long we wait to debounce resize events
 | 
						|
const LAZY_RESIZE_INTERVAL_MS = 200;
 | 
						|
 | 
						|
// If the toolbox's width is smaller than the given amount of pixels, the sidebar
 | 
						|
// automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
 | 
						|
const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
 | 
						|
// If the toolbox's width docked to the side is smaller than the given amount of pixels,
 | 
						|
// the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
 | 
						|
// mode.
 | 
						|
const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
 | 
						|
 | 
						|
const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
 | 
						|
const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
 | 
						|
const THREE_PANE_CHROME_ENABLED_PREF =
 | 
						|
  "devtools.inspector.chrome.three-pane-enabled";
 | 
						|
const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
 | 
						|
const TELEMETRY_SCALAR_NODE_SELECTION_COUNT =
 | 
						|
  "devtools.inspector.node_selection_count";
 | 
						|
 | 
						|
/**
 | 
						|
 * Represents an open instance of the Inspector for a tab.
 | 
						|
 * The inspector controls the breadcrumbs, the markup view, and the sidebar
 | 
						|
 * (computed view, rule view, font view and animation inspector).
 | 
						|
 *
 | 
						|
 * Events:
 | 
						|
 * - ready
 | 
						|
 *      Fired when the inspector panel is opened for the first time and ready to
 | 
						|
 *      use
 | 
						|
 * - new-root
 | 
						|
 *      Fired after a new root (navigation to a new page) event was fired by
 | 
						|
 *      the walker, and taken into account by the inspector (after the markup
 | 
						|
 *      view has been reloaded)
 | 
						|
 * - markuploaded
 | 
						|
 *      Fired when the markup-view frame has loaded
 | 
						|
 * - breadcrumbs-updated
 | 
						|
 *      Fired when the breadcrumb widget updates to a new node
 | 
						|
 * - boxmodel-view-updated
 | 
						|
 *      Fired when the box model updates to a new node
 | 
						|
 * - markupmutation
 | 
						|
 *      Fired after markup mutations have been processed by the markup-view
 | 
						|
 * - computed-view-refreshed
 | 
						|
 *      Fired when the computed rules view updates to a new node
 | 
						|
 * - computed-view-property-expanded
 | 
						|
 *      Fired when a property is expanded in the computed rules view
 | 
						|
 * - computed-view-property-collapsed
 | 
						|
 *      Fired when a property is collapsed in the computed rules view
 | 
						|
 * - computed-view-sourcelinks-updated
 | 
						|
 *      Fired when the stylesheet source links have been updated (when switching
 | 
						|
 *      to source-mapped files)
 | 
						|
 * - rule-view-refreshed
 | 
						|
 *      Fired when the rule view updates to a new node
 | 
						|
 * - rule-view-sourcelinks-updated
 | 
						|
 *      Fired when the stylesheet source links have been updated (when switching
 | 
						|
 *      to source-mapped files)
 | 
						|
 */
 | 
						|
function Inspector(toolbox, commands) {
 | 
						|
  EventEmitter.decorate(this);
 | 
						|
 | 
						|
  this._toolbox = toolbox;
 | 
						|
  this._commands = commands;
 | 
						|
  this.panelDoc = window.document;
 | 
						|
  this.panelWin = window;
 | 
						|
  this.panelWin.inspector = this;
 | 
						|
  this.telemetry = toolbox.telemetry;
 | 
						|
  this.store = createStore(this);
 | 
						|
 | 
						|
  // Map [panel id => panel instance]
 | 
						|
  // Stores all the instances of sidebar panels like rule view, computed view, ...
 | 
						|
  this._panels = new Map();
 | 
						|
 | 
						|
  this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
 | 
						|
  this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(
 | 
						|
    this
 | 
						|
  );
 | 
						|
  this._onTargetAvailable = this._onTargetAvailable.bind(this);
 | 
						|
  this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
 | 
						|
  this._onWillNavigate = this._onWillNavigate.bind(this);
 | 
						|
  this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
 | 
						|
 | 
						|
  this.onDetached = this.onDetached.bind(this);
 | 
						|
  this.onHostChanged = this.onHostChanged.bind(this);
 | 
						|
  this.onNewSelection = this.onNewSelection.bind(this);
 | 
						|
  this.onResourceAvailable = this.onResourceAvailable.bind(this);
 | 
						|
  this.onRootNodeAvailable = this.onRootNodeAvailable.bind(this);
 | 
						|
  this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
 | 
						|
  this.onPickerCanceled = this.onPickerCanceled.bind(this);
 | 
						|
  this.onPickerHovered = this.onPickerHovered.bind(this);
 | 
						|
  this.onPickerPicked = this.onPickerPicked.bind(this);
 | 
						|
  this.onSidebarHidden = this.onSidebarHidden.bind(this);
 | 
						|
  this.onSidebarResized = this.onSidebarResized.bind(this);
 | 
						|
  this.onSidebarSelect = this.onSidebarSelect.bind(this);
 | 
						|
  this.onSidebarShown = this.onSidebarShown.bind(this);
 | 
						|
  this.onSidebarToggle = this.onSidebarToggle.bind(this);
 | 
						|
  this.onReflowInSelection = this.onReflowInSelection.bind(this);
 | 
						|
  this.listenForSearchEvents = this.listenForSearchEvents.bind(this);
 | 
						|
}
 | 
						|
 | 
						|
Inspector.prototype = {
 | 
						|
  /**
 | 
						|
   * InspectorPanel.open() is effectively an asynchronous constructor.
 | 
						|
   * Set any attributes or listeners that rely on the document being loaded or fronts
 | 
						|
   * from the InspectorFront and Target here.
 | 
						|
   */
 | 
						|
  async init() {
 | 
						|
    // Localize all the nodes containing a data-localization attribute.
 | 
						|
    localizeMarkup(this.panelDoc);
 | 
						|
 | 
						|
    this._fluentL10n = new FluentL10n();
 | 
						|
    await this._fluentL10n.init(["devtools/client/compatibility.ftl"]);
 | 
						|
 | 
						|
    // Display the main inspector panel with: search input, markup view and breadcrumbs.
 | 
						|
    this.panelDoc.getElementById("inspector-main-content").style.visibility =
 | 
						|
      "visible";
 | 
						|
 | 
						|
    // Setup the splitter before watching targets & resources.
 | 
						|
    // The markup view will be initialized after we get the first root-node
 | 
						|
    // resource, and the splitter should be initialized before that.
 | 
						|
    // The markup view is rendered in an iframe and the splitter will move the
 | 
						|
    // parent of the iframe in the DOM tree which would reset the state of the
 | 
						|
    // iframe if it had already been initialized.
 | 
						|
    this.setupSplitter();
 | 
						|
 | 
						|
    await this.commands.targetCommand.watchTargets(
 | 
						|
      [this.commands.targetCommand.TYPES.FRAME],
 | 
						|
      this._onTargetAvailable,
 | 
						|
      this._onTargetDestroyed
 | 
						|
    );
 | 
						|
 | 
						|
    await this.toolbox.resourceCommand.watchResources(
 | 
						|
      [
 | 
						|
        this.toolbox.resourceCommand.TYPES.ROOT_NODE,
 | 
						|
        // To observe CSS change before opening changes view.
 | 
						|
        this.toolbox.resourceCommand.TYPES.CSS_CHANGE,
 | 
						|
        this.toolbox.resourceCommand.TYPES.DOCUMENT_EVENT,
 | 
						|
      ],
 | 
						|
      { onAvailable: this.onResourceAvailable }
 | 
						|
    );
 | 
						|
 | 
						|
    // Store the URL of the target page prior to navigation in order to ensure
 | 
						|
    // telemetry counts in the Grid Inspector are not double counted on reload.
 | 
						|
    this.previousURL = this.currentTarget.url;
 | 
						|
 | 
						|
    // Note: setupSidebar() really has to be called after the first target has
 | 
						|
    // been processed, so that the cssProperties getter works.
 | 
						|
    // But the rest could be moved before the watch* calls.
 | 
						|
    this.styleChangeTracker = new InspectorStyleChangeTracker(this);
 | 
						|
    this.setupSidebar();
 | 
						|
    this.breadcrumbs = new HTMLBreadcrumbs(this);
 | 
						|
    this.setupExtensionSidebars();
 | 
						|
    this.setupSearchBox();
 | 
						|
 | 
						|
    this.onNewSelection();
 | 
						|
 | 
						|
    this.toolbox.on("host-changed", this.onHostChanged);
 | 
						|
    this.toolbox.nodePicker.on("picker-node-hovered", this.onPickerHovered);
 | 
						|
    this.toolbox.nodePicker.on("picker-node-canceled", this.onPickerCanceled);
 | 
						|
    this.toolbox.nodePicker.on("picker-node-picked", this.onPickerPicked);
 | 
						|
    this.selection.on("new-node-front", this.onNewSelection);
 | 
						|
    this.selection.on("detached-front", this.onDetached);
 | 
						|
 | 
						|
    // Log the 3 pane inspector setting on inspector open. The question we want to answer
 | 
						|
    // is:
 | 
						|
    // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
 | 
						|
    this.telemetry.keyedScalarAdd(
 | 
						|
      THREE_PANE_ENABLED_SCALAR,
 | 
						|
      this.is3PaneModeEnabled,
 | 
						|
      1
 | 
						|
    );
 | 
						|
 | 
						|
    return this;
 | 
						|
  },
 | 
						|
 | 
						|
  async _onTargetAvailable({ targetFront }) {
 | 
						|
    // Ignore all targets but the top level one
 | 
						|
    if (!targetFront.isTopLevel) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.initInspectorFront(targetFront);
 | 
						|
 | 
						|
    // the target might have been destroyed when reloading quickly,
 | 
						|
    // while waiting for inspector front initialization
 | 
						|
    if (targetFront.isDestroyed()) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await Promise.all([
 | 
						|
      this._getCssProperties(targetFront),
 | 
						|
      this._getAccessibilityFront(targetFront),
 | 
						|
    ]);
 | 
						|
  },
 | 
						|
 | 
						|
  _onTargetDestroyed({ targetFront }) {
 | 
						|
    // Ignore all targets but the top level one
 | 
						|
    if (!targetFront.isTopLevel) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._defaultNode = null;
 | 
						|
    this.selection.setNodeFront(null);
 | 
						|
  },
 | 
						|
 | 
						|
  onResourceAvailable: function(resources) {
 | 
						|
    // Store all onRootNodeAvailable calls which are asynchronous.
 | 
						|
    const rootNodeAvailablePromises = [];
 | 
						|
 | 
						|
    for (const resource of resources) {
 | 
						|
      const isTopLevelTarget = !!resource.targetFront?.isTopLevel;
 | 
						|
      const isTopLevelDocument = !!resource.isTopLevelDocument;
 | 
						|
      if (
 | 
						|
        resource.resourceType ===
 | 
						|
          this.toolbox.resourceCommand.TYPES.ROOT_NODE &&
 | 
						|
        // It might happen that the ROOT_NODE resource (which is a Front) is already
 | 
						|
        // destroyed, and in such case we want to ignore it.
 | 
						|
        !resource.isDestroyed() &&
 | 
						|
        isTopLevelTarget &&
 | 
						|
        isTopLevelDocument
 | 
						|
      ) {
 | 
						|
        rootNodeAvailablePromises.push(this.onRootNodeAvailable(resource));
 | 
						|
      }
 | 
						|
 | 
						|
      // Only consider top level document, and ignore remote iframes top document
 | 
						|
      if (
 | 
						|
        resource.resourceType ===
 | 
						|
          this.toolbox.resourceCommand.TYPES.DOCUMENT_EVENT &&
 | 
						|
        resource.name === "will-navigate" &&
 | 
						|
        isTopLevelTarget
 | 
						|
      ) {
 | 
						|
        this._onWillNavigate();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return Promise.all(rootNodeAvailablePromises);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Reset the inspector on new root mutation.
 | 
						|
   */
 | 
						|
  onRootNodeAvailable: async function(rootNodeFront) {
 | 
						|
    // Record new-root timing for telemetry
 | 
						|
    this._newRootStart = this.panelWin.performance.now();
 | 
						|
 | 
						|
    this._defaultNode = null;
 | 
						|
    this.selection.setNodeFront(null);
 | 
						|
    this._destroyMarkup();
 | 
						|
 | 
						|
    try {
 | 
						|
      const defaultNode = await this._getDefaultNodeForSelection(rootNodeFront);
 | 
						|
      if (!defaultNode) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      this.selection.setNodeFront(defaultNode, {
 | 
						|
        reason: "inspector-default-selection",
 | 
						|
      });
 | 
						|
 | 
						|
      await this._initMarkupView();
 | 
						|
 | 
						|
      // Setup the toolbar again, since its content may depend on the current document.
 | 
						|
      this.setupToolbar();
 | 
						|
    } catch (e) {
 | 
						|
      this._handleRejectionIfNotDestroyed(e);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _initMarkupView: async function() {
 | 
						|
    if (!this._markupFrame) {
 | 
						|
      this._markupFrame = this.panelDoc.createElement("iframe");
 | 
						|
      this._markupFrame.setAttribute(
 | 
						|
        "aria-label",
 | 
						|
        INSPECTOR_L10N.getStr("inspector.panelLabel.markupView")
 | 
						|
      );
 | 
						|
      this._markupFrame.setAttribute("flex", "1");
 | 
						|
      // This is needed to enable tooltips inside the iframe document.
 | 
						|
      this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
 | 
						|
 | 
						|
      this._markupBox = this.panelDoc.getElementById("markup-box");
 | 
						|
      this._markupBox.style.visibility = "hidden";
 | 
						|
      this._markupBox.appendChild(this._markupFrame);
 | 
						|
 | 
						|
      const onMarkupFrameLoaded = new Promise(r =>
 | 
						|
        this._markupFrame.addEventListener("load", r, {
 | 
						|
          capture: true,
 | 
						|
          once: true,
 | 
						|
        })
 | 
						|
      );
 | 
						|
 | 
						|
      this._markupFrame.setAttribute("src", "markup/markup.xhtml");
 | 
						|
 | 
						|
      await onMarkupFrameLoaded;
 | 
						|
    }
 | 
						|
 | 
						|
    this._markupFrame.contentWindow.focus();
 | 
						|
    this._markupBox.style.visibility = "visible";
 | 
						|
    this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
 | 
						|
    // TODO: We might be able to merge markuploaded, new-root and reloaded.
 | 
						|
    this.emitForTests("markuploaded");
 | 
						|
 | 
						|
    const onExpand = this.markup.expandNode(this.selection.nodeFront);
 | 
						|
 | 
						|
    // Restore the highlighter states prior to emitting "new-root".
 | 
						|
    if (this._highlighters) {
 | 
						|
      await Promise.all([
 | 
						|
        this.highlighters.restoreFlexboxState(),
 | 
						|
        this.highlighters.restoreGridState(),
 | 
						|
      ]);
 | 
						|
    }
 | 
						|
    this.emit("new-root");
 | 
						|
 | 
						|
    // Wait for full expand of the selected node in order to ensure
 | 
						|
    // the markup view is fully emitted before firing 'reloaded'.
 | 
						|
    // 'reloaded' is used to know when the panel is fully updated
 | 
						|
    // after a page reload.
 | 
						|
    await onExpand;
 | 
						|
 | 
						|
    this.emit("reloaded");
 | 
						|
 | 
						|
    // Record the time between new-root event and inspector fully loaded.
 | 
						|
    if (this._newRootStart) {
 | 
						|
      // Only log the timing when inspector is not destroyed and is in foreground.
 | 
						|
      if (this.toolbox && this.toolbox.currentToolId == "inspector") {
 | 
						|
        const delay = this.panelWin.performance.now() - this._newRootStart;
 | 
						|
        const telemetryKey = "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS";
 | 
						|
        const histogram = this.telemetry.getHistogramById(telemetryKey);
 | 
						|
        histogram.add(delay);
 | 
						|
      }
 | 
						|
      delete this._newRootStart;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async initInspectorFront(targetFront) {
 | 
						|
    this.inspectorFront = await targetFront.getFront("inspector");
 | 
						|
    this.walker = this.inspectorFront.walker;
 | 
						|
  },
 | 
						|
 | 
						|
  get toolbox() {
 | 
						|
    return this._toolbox;
 | 
						|
  },
 | 
						|
 | 
						|
  get commands() {
 | 
						|
    return this._commands;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the list of InspectorFront instances that correspond to all of the inspectable
 | 
						|
   * targets in remote frames nested within the document inspected here, as well as the
 | 
						|
   * current InspectorFront instance.
 | 
						|
   *
 | 
						|
   * @return {Array} The list of InspectorFront instances.
 | 
						|
   */
 | 
						|
  async getAllInspectorFronts() {
 | 
						|
    return this.commands.targetCommand.getAllFronts(
 | 
						|
      [this.commands.targetCommand.TYPES.FRAME],
 | 
						|
      "inspector"
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  get highlighters() {
 | 
						|
    if (!this._highlighters) {
 | 
						|
      this._highlighters = new HighlightersOverlay(this);
 | 
						|
    }
 | 
						|
 | 
						|
    return this._highlighters;
 | 
						|
  },
 | 
						|
 | 
						|
  get is3PaneModeEnabled() {
 | 
						|
    if (this.currentTarget.chrome) {
 | 
						|
      if (!this._is3PaneModeChromeEnabled) {
 | 
						|
        this._is3PaneModeChromeEnabled = Services.prefs.getBoolPref(
 | 
						|
          THREE_PANE_CHROME_ENABLED_PREF
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      return this._is3PaneModeChromeEnabled;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!this._is3PaneModeEnabled) {
 | 
						|
      this._is3PaneModeEnabled = Services.prefs.getBoolPref(
 | 
						|
        THREE_PANE_ENABLED_PREF
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return this._is3PaneModeEnabled;
 | 
						|
  },
 | 
						|
 | 
						|
  set is3PaneModeEnabled(value) {
 | 
						|
    if (this.currentTarget.chrome) {
 | 
						|
      this._is3PaneModeChromeEnabled = value;
 | 
						|
      Services.prefs.setBoolPref(
 | 
						|
        THREE_PANE_CHROME_ENABLED_PREF,
 | 
						|
        this._is3PaneModeChromeEnabled
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      this._is3PaneModeEnabled = value;
 | 
						|
      Services.prefs.setBoolPref(
 | 
						|
        THREE_PANE_ENABLED_PREF,
 | 
						|
        this._is3PaneModeEnabled
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  get search() {
 | 
						|
    if (!this._search) {
 | 
						|
      this._search = new InspectorSearch(
 | 
						|
        this,
 | 
						|
        this.searchBox,
 | 
						|
        this.searchClearButton
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return this._search;
 | 
						|
  },
 | 
						|
 | 
						|
  get selection() {
 | 
						|
    return this.toolbox.selection;
 | 
						|
  },
 | 
						|
 | 
						|
  get cssProperties() {
 | 
						|
    return this._cssProperties.cssProperties;
 | 
						|
  },
 | 
						|
 | 
						|
  get fluentL10n() {
 | 
						|
    return this._fluentL10n;
 | 
						|
  },
 | 
						|
 | 
						|
  // Duration in milliseconds after which to hide the highlighter for the picked node.
 | 
						|
  // While testing, disable auto hiding to prevent intermittent test failures.
 | 
						|
  // Some tests are very slow. If the highlighter is hidden after a delay, the test may
 | 
						|
  // find itself midway through without a highlighter to test.
 | 
						|
  // This value is exposed on Inspector so individual tests can restore it when needed.
 | 
						|
  HIGHLIGHTER_AUTOHIDE_TIMER: flags.testing ? 0 : 1000,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle promise rejections for various asynchronous actions, and only log errors if
 | 
						|
   * the inspector panel still exists.
 | 
						|
   * This is useful to silence useless errors that happen when the inspector is closed
 | 
						|
   * while still initializing (and making protocol requests).
 | 
						|
   */
 | 
						|
  _handleRejectionIfNotDestroyed: function(e) {
 | 
						|
    if (!this._destroyed) {
 | 
						|
      console.error(e);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _onWillNavigate: function() {
 | 
						|
    this._defaultNode = null;
 | 
						|
    this.selection.setNodeFront(null);
 | 
						|
    if (this._highlighters) {
 | 
						|
      this._highlighters.onWillNavigate();
 | 
						|
    }
 | 
						|
    this._destroyMarkup();
 | 
						|
    this._pendingSelectionUnique = null;
 | 
						|
  },
 | 
						|
 | 
						|
  _getCssProperties: async function(targetFront) {
 | 
						|
    this._cssProperties = await targetFront.getFront("cssProperties");
 | 
						|
  },
 | 
						|
 | 
						|
  _getAccessibilityFront: async function(targetFront) {
 | 
						|
    this.accessibilityFront = await targetFront.getFront("accessibility");
 | 
						|
    return this.accessibilityFront;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return a promise that will resolve to the default node for selection.
 | 
						|
   *
 | 
						|
   * @param {NodeFront} rootNodeFront
 | 
						|
   *        The current root node front for the top walker.
 | 
						|
   */
 | 
						|
  _getDefaultNodeForSelection: async function(rootNodeFront) {
 | 
						|
    if (this._defaultNode) {
 | 
						|
      return this._defaultNode;
 | 
						|
    }
 | 
						|
 | 
						|
    // Save the _pendingSelectionUnique on the current inspector instance.
 | 
						|
    const pendingSelectionUnique = Symbol("pending-selection");
 | 
						|
    this._pendingSelectionUnique = pendingSelectionUnique;
 | 
						|
 | 
						|
    if (this._pendingSelectionUnique !== pendingSelectionUnique) {
 | 
						|
      // If this method was called again while waiting, bail out.
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    const walker = this.walker;
 | 
						|
    const cssSelectors = this.selectionCssSelectors;
 | 
						|
    // Try to find a default node using three strategies:
 | 
						|
    const defaultNodeSelectors = [
 | 
						|
      // - first try to match css selectors for the selection
 | 
						|
      () => (cssSelectors.length ? walker.findNodeFront(cssSelectors) : null),
 | 
						|
      // - otherwise try to get the "body" element
 | 
						|
      () => walker.querySelector(rootNodeFront, "body"),
 | 
						|
      // - finally get the documentElement element if nothing else worked.
 | 
						|
      () => walker.documentElement(),
 | 
						|
    ];
 | 
						|
 | 
						|
    // Try all default node selectors until a valid node is found.
 | 
						|
    for (const selector of defaultNodeSelectors) {
 | 
						|
      const node = await selector();
 | 
						|
      if (this._pendingSelectionUnique !== pendingSelectionUnique) {
 | 
						|
        // If this method was called again while waiting, bail out.
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      if (node) {
 | 
						|
        this._defaultNode = node;
 | 
						|
        return node;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Top level target front getter.
 | 
						|
   */
 | 
						|
  get currentTarget() {
 | 
						|
    return this.commands.targetCommand.targetFront;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Hooks the searchbar to show result and auto completion suggestions.
 | 
						|
   */
 | 
						|
  setupSearchBox: function() {
 | 
						|
    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
 | 
						|
    this.searchClearButton = this.panelDoc.getElementById(
 | 
						|
      "inspector-searchinput-clear"
 | 
						|
    );
 | 
						|
    this.searchResultsContainer = this.panelDoc.getElementById(
 | 
						|
      "inspector-searchlabel-container"
 | 
						|
    );
 | 
						|
    this.searchResultsLabel = this.panelDoc.getElementById(
 | 
						|
      "inspector-searchlabel"
 | 
						|
    );
 | 
						|
 | 
						|
    this.searchBox.addEventListener("focus", this.listenForSearchEvents, {
 | 
						|
      once: true,
 | 
						|
    });
 | 
						|
 | 
						|
    this.createSearchBoxShortcuts();
 | 
						|
  },
 | 
						|
 | 
						|
  listenForSearchEvents() {
 | 
						|
    this.search.on("search-cleared", this._clearSearchResultsLabel);
 | 
						|
    this.search.on("search-result", this._updateSearchResultsLabel);
 | 
						|
  },
 | 
						|
 | 
						|
  createSearchBoxShortcuts() {
 | 
						|
    this.searchboxShortcuts = new KeyShortcuts({
 | 
						|
      window: this.panelDoc.defaultView,
 | 
						|
      // The inspector search shortcuts need to be available from everywhere in the
 | 
						|
      // inspector, and the inspector uses iframes (markupview, sidepanel webextensions).
 | 
						|
      // Use the chromeEventHandler as the target to catch events from all frames.
 | 
						|
      target: this.toolbox.getChromeEventHandler(),
 | 
						|
    });
 | 
						|
    const key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
 | 
						|
    this.searchboxShortcuts.on(key, event => {
 | 
						|
      // Prevent overriding same shortcut from the computed/rule views
 | 
						|
      if (
 | 
						|
        event.originalTarget.closest("#sidebar-panel-ruleview") ||
 | 
						|
        event.originalTarget.closest("#sidebar-panel-computedview")
 | 
						|
      ) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      const win = event.originalTarget.ownerGlobal;
 | 
						|
      // Check if the event is coming from an inspector window to avoid catching
 | 
						|
      // events from other panels. Note, we are testing both win and win.parent
 | 
						|
      // because the inspector uses iframes.
 | 
						|
      if (win === this.panelWin || win.parent === this.panelWin) {
 | 
						|
        event.preventDefault();
 | 
						|
        this.searchBox.focus();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  get searchSuggestions() {
 | 
						|
    return this.search.autocompleter;
 | 
						|
  },
 | 
						|
 | 
						|
  _clearSearchResultsLabel: function(result) {
 | 
						|
    return this._updateSearchResultsLabel(result, true);
 | 
						|
  },
 | 
						|
 | 
						|
  _updateSearchResultsLabel: function(result, clear = false) {
 | 
						|
    let str = "";
 | 
						|
    if (!clear) {
 | 
						|
      if (result) {
 | 
						|
        str = INSPECTOR_L10N.getFormatStr(
 | 
						|
          "inspector.searchResultsCount2",
 | 
						|
          result.resultsIndex + 1,
 | 
						|
          result.resultsLength
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
 | 
						|
      }
 | 
						|
 | 
						|
      this.searchResultsContainer.hidden = false;
 | 
						|
    } else {
 | 
						|
      this.searchResultsContainer.hidden = true;
 | 
						|
    }
 | 
						|
 | 
						|
    this.searchResultsLabel.textContent = str;
 | 
						|
  },
 | 
						|
 | 
						|
  get React() {
 | 
						|
    return this._toolbox.React;
 | 
						|
  },
 | 
						|
 | 
						|
  get ReactDOM() {
 | 
						|
    return this._toolbox.ReactDOM;
 | 
						|
  },
 | 
						|
 | 
						|
  get ReactRedux() {
 | 
						|
    return this._toolbox.ReactRedux;
 | 
						|
  },
 | 
						|
 | 
						|
  get browserRequire() {
 | 
						|
    return this._toolbox.browserRequire;
 | 
						|
  },
 | 
						|
 | 
						|
  get InspectorTabPanel() {
 | 
						|
    if (!this._InspectorTabPanel) {
 | 
						|
      this._InspectorTabPanel = this.React.createFactory(
 | 
						|
        this.browserRequire(
 | 
						|
          "devtools/client/inspector/components/InspectorTabPanel"
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return this._InspectorTabPanel;
 | 
						|
  },
 | 
						|
 | 
						|
  get InspectorSplitBox() {
 | 
						|
    if (!this._InspectorSplitBox) {
 | 
						|
      this._InspectorSplitBox = this.React.createFactory(
 | 
						|
        this.browserRequire(
 | 
						|
          "devtools/client/shared/components/splitter/SplitBox"
 | 
						|
        )
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return this._InspectorSplitBox;
 | 
						|
  },
 | 
						|
 | 
						|
  get TabBar() {
 | 
						|
    if (!this._TabBar) {
 | 
						|
      this._TabBar = this.React.createFactory(
 | 
						|
        this.browserRequire("devtools/client/shared/components/tabs/TabBar")
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return this._TabBar;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check if the inspector should use the landscape mode.
 | 
						|
   *
 | 
						|
   * @return {Boolean} true if the inspector should be in landscape mode.
 | 
						|
   */
 | 
						|
  useLandscapeMode: function() {
 | 
						|
    if (!this.panelDoc) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
 | 
						|
    const width = splitterBox.clientWidth;
 | 
						|
 | 
						|
    return this.is3PaneModeEnabled &&
 | 
						|
      (this.toolbox.hostType == Toolbox.HostType.LEFT ||
 | 
						|
        this.toolbox.hostType == Toolbox.HostType.RIGHT)
 | 
						|
      ? width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD
 | 
						|
      : width > PORTRAIT_MODE_WIDTH_THRESHOLD;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build Splitter located between the main and side area of
 | 
						|
   * the Inspector panel.
 | 
						|
   */
 | 
						|
  setupSplitter: function() {
 | 
						|
    const { width, height, splitSidebarWidth } = this.getSidebarSize();
 | 
						|
 | 
						|
    this.sidebarSplitBoxRef = this.React.createRef();
 | 
						|
 | 
						|
    const splitter = this.InspectorSplitBox({
 | 
						|
      className: "inspector-sidebar-splitter",
 | 
						|
      initialWidth: width,
 | 
						|
      initialHeight: height,
 | 
						|
      minSize: "10%",
 | 
						|
      maxSize: "80%",
 | 
						|
      splitterSize: 1,
 | 
						|
      endPanelControl: true,
 | 
						|
      startPanel: this.InspectorTabPanel({
 | 
						|
        id: "inspector-main-content",
 | 
						|
      }),
 | 
						|
      endPanel: this.InspectorSplitBox({
 | 
						|
        initialWidth: splitSidebarWidth,
 | 
						|
        minSize: "225px",
 | 
						|
        maxSize: "80%",
 | 
						|
        splitterSize: this.is3PaneModeEnabled ? 1 : 0,
 | 
						|
        endPanelControl: this.is3PaneModeEnabled,
 | 
						|
        startPanel: this.InspectorTabPanel({
 | 
						|
          id: "inspector-rules-container",
 | 
						|
        }),
 | 
						|
        endPanel: this.InspectorTabPanel({
 | 
						|
          id: "inspector-sidebar-container",
 | 
						|
        }),
 | 
						|
        ref: this.sidebarSplitBoxRef,
 | 
						|
      }),
 | 
						|
      vert: this.useLandscapeMode(),
 | 
						|
      onControlledPanelResized: this.onSidebarResized,
 | 
						|
    });
 | 
						|
 | 
						|
    this.splitBox = this.ReactDOM.render(
 | 
						|
      splitter,
 | 
						|
      this.panelDoc.getElementById("inspector-splitter-box")
 | 
						|
    );
 | 
						|
 | 
						|
    this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
 | 
						|
  },
 | 
						|
 | 
						|
  _onLazyPanelResize: async function() {
 | 
						|
    // We can be called on a closed window or destroyed toolbox because of the deferred task.
 | 
						|
    if (window.closed || this._destroyed) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.splitBox.setState({ vert: this.useLandscapeMode() });
 | 
						|
    this.emit("inspector-resize");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If Toolbox width is less than 600 px, the splitter changes its mode
 | 
						|
   * to `horizontal` to support portrait view.
 | 
						|
   */
 | 
						|
  onPanelWindowResize: function() {
 | 
						|
    if (this.toolbox.currentToolId !== "inspector") {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!this._lazyResizeHandler) {
 | 
						|
      this._lazyResizeHandler = new DeferredTask(
 | 
						|
        this._onLazyPanelResize.bind(this),
 | 
						|
        LAZY_RESIZE_INTERVAL_MS,
 | 
						|
        0
 | 
						|
      );
 | 
						|
    }
 | 
						|
    this._lazyResizeHandler.arm();
 | 
						|
  },
 | 
						|
 | 
						|
  getSidebarSize: function() {
 | 
						|
    let width;
 | 
						|
    let height;
 | 
						|
    let splitSidebarWidth;
 | 
						|
 | 
						|
    // Initialize splitter size from preferences.
 | 
						|
    try {
 | 
						|
      width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
 | 
						|
      height = Services.prefs.getIntPref(
 | 
						|
        "devtools.toolsidebar-height.inspector"
 | 
						|
      );
 | 
						|
      splitSidebarWidth = Services.prefs.getIntPref(
 | 
						|
        "devtools.toolsidebar-width.inspector.splitsidebar"
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      // Set width and height of the splitter. Only one
 | 
						|
      // value is really useful at a time depending on the current
 | 
						|
      // orientation (vertical/horizontal).
 | 
						|
      // Having both is supported by the splitter component.
 | 
						|
      width = this.is3PaneModeEnabled
 | 
						|
        ? INITIAL_SIDEBAR_SIZE * 2
 | 
						|
        : INITIAL_SIDEBAR_SIZE;
 | 
						|
      height = INITIAL_SIDEBAR_SIZE;
 | 
						|
      splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
 | 
						|
    }
 | 
						|
 | 
						|
    return { width, height, splitSidebarWidth };
 | 
						|
  },
 | 
						|
 | 
						|
  onSidebarHidden: function() {
 | 
						|
    // Store the current splitter size to preferences.
 | 
						|
    const state = this.splitBox.state;
 | 
						|
    Services.prefs.setIntPref(
 | 
						|
      "devtools.toolsidebar-width.inspector",
 | 
						|
      state.width
 | 
						|
    );
 | 
						|
    Services.prefs.setIntPref(
 | 
						|
      "devtools.toolsidebar-height.inspector",
 | 
						|
      state.height
 | 
						|
    );
 | 
						|
    Services.prefs.setIntPref(
 | 
						|
      "devtools.toolsidebar-width.inspector.splitsidebar",
 | 
						|
      this.sidebarSplitBoxRef.current.state.width
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  onSidebarResized: function(width, height) {
 | 
						|
    this.toolbox.emit("inspector-sidebar-resized", { width, height });
 | 
						|
  },
 | 
						|
 | 
						|
  onSidebarSelect: function(toolId) {
 | 
						|
    // Save the currently selected sidebar panel
 | 
						|
    Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
 | 
						|
 | 
						|
    // Then forces the panel creation by calling getPanel
 | 
						|
    // (This allows lazy loading the panels only once we select them)
 | 
						|
    this.getPanel(toolId);
 | 
						|
 | 
						|
    this.toolbox.emit("inspector-sidebar-select", toolId);
 | 
						|
  },
 | 
						|
 | 
						|
  onSidebarShown: function() {
 | 
						|
    const { width, height, splitSidebarWidth } = this.getSidebarSize();
 | 
						|
    this.splitBox.setState({ width, height });
 | 
						|
    this.sidebarSplitBoxRef.current.setState({ width: splitSidebarWidth });
 | 
						|
  },
 | 
						|
 | 
						|
  async onSidebarToggle() {
 | 
						|
    this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
 | 
						|
    await this.setupToolbar();
 | 
						|
    this.addRuleView({ skipQueue: true });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
 | 
						|
   * split box, specifies the end panel control and resizes the split box width depending
 | 
						|
   * on the width of the toolbox.
 | 
						|
   */
 | 
						|
  setSidebarSplitBoxState() {
 | 
						|
    const toolboxWidth = this.panelDoc.getElementById("inspector-splitter-box")
 | 
						|
      .clientWidth;
 | 
						|
 | 
						|
    // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
 | 
						|
    // vertical mode) width.
 | 
						|
    const sidebarWidth = this.splitBox.state.width;
 | 
						|
    // This variable represents the width of the right panel in horizontal mode or
 | 
						|
    // bottom-right panel in vertical mode width in 3 pane mode.
 | 
						|
    let sidebarSplitboxWidth;
 | 
						|
 | 
						|
    if (this.useLandscapeMode()) {
 | 
						|
      // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
 | 
						|
      // or bottom panel in vertical mode) width will be bigger than half of the
 | 
						|
      // toolbox's width.
 | 
						|
      const canDoubleSidebarWidth = sidebarWidth * 2 < toolboxWidth / 2;
 | 
						|
 | 
						|
      // Resize the main split box's end panel that contains the middle and right panel.
 | 
						|
      // Attempts to resize the main split box's end panel to be double the size of the
 | 
						|
      // existing sidebar's width when switching to 3 pane mode. However, if the middle
 | 
						|
      // and right panel's width together is greater than half of the toolbox's width,
 | 
						|
      // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
 | 
						|
      // the current toolbox's width.
 | 
						|
      this.splitBox.setState({
 | 
						|
        width: canDoubleSidebarWidth
 | 
						|
          ? sidebarWidth * 2
 | 
						|
          : (toolboxWidth * 2) / 3,
 | 
						|
      });
 | 
						|
 | 
						|
      // In landscape/horizontal mode, set the right panel back to its original
 | 
						|
      // inspector sidebar width if we can double the sidebar width. Otherwise, set
 | 
						|
      // the width of the right panel to be 1/3 of the toolbox's width since all 3
 | 
						|
      // panels will be equally sized.
 | 
						|
      sidebarSplitboxWidth = canDoubleSidebarWidth
 | 
						|
        ? sidebarWidth
 | 
						|
        : toolboxWidth / 3;
 | 
						|
    } else {
 | 
						|
      // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
 | 
						|
      // toolbox's width.
 | 
						|
      sidebarSplitboxWidth = toolboxWidth / 2;
 | 
						|
    }
 | 
						|
 | 
						|
    // Show the splitter inside the sidebar split box. Sets the width of the inspector
 | 
						|
    // sidebar and specify that the end (right in horizontal or bottom-right in
 | 
						|
    // vertical) panel of the sidebar split box should be controlled when resizing.
 | 
						|
    this.sidebarSplitBoxRef.current.setState({
 | 
						|
      endPanelControl: true,
 | 
						|
      splitterSize: 1,
 | 
						|
      width: sidebarSplitboxWidth,
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
 | 
						|
   * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
 | 
						|
   * pane mode. The default tab specifies whether or not the rule view should be selected.
 | 
						|
   * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
 | 
						|
   * rule view is being merged back into the inspector sidebar from middle/bottom-left
 | 
						|
   * panel. Otherwise, we specify the default tab when handling the sidebar setup.
 | 
						|
   *
 | 
						|
   * @params {String} defaultTab
 | 
						|
   *         Thie id of the default tab for the sidebar.
 | 
						|
   */
 | 
						|
  addRuleView({ defaultTab = "ruleview", skipQueue = false } = {}) {
 | 
						|
    const ruleViewSidebar = this.sidebarSplitBoxRef.current.startPanelContainer;
 | 
						|
 | 
						|
    if (this.is3PaneModeEnabled) {
 | 
						|
      // Convert to 3 pane mode by removing the rule view from the inspector sidebar
 | 
						|
      // and adding the rule view to the middle (in landscape/horizontal mode) or
 | 
						|
      // bottom-left (in portrait/vertical mode) panel.
 | 
						|
      ruleViewSidebar.style.display = "block";
 | 
						|
 | 
						|
      this.setSidebarSplitBoxState();
 | 
						|
 | 
						|
      // Force the rule view panel creation by calling getPanel
 | 
						|
      this.getPanel("ruleview");
 | 
						|
 | 
						|
      this.sidebar.removeTab("ruleview");
 | 
						|
 | 
						|
      this.ruleViewSideBar.addExistingTab(
 | 
						|
        "ruleview",
 | 
						|
        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
 | 
						|
        true
 | 
						|
      );
 | 
						|
 | 
						|
      this.ruleViewSideBar.show();
 | 
						|
    } else {
 | 
						|
      // Removes the rule view from the 3 pane mode and adds the rule view to the main
 | 
						|
      // inspector sidebar.
 | 
						|
      ruleViewSidebar.style.display = "none";
 | 
						|
 | 
						|
      // Set the width of the split box (right panel in horziontal mode and bottom panel
 | 
						|
      // in vertical mode) to be the width of the inspector sidebar.
 | 
						|
      const splitterBox = this.panelDoc.getElementById(
 | 
						|
        "inspector-splitter-box"
 | 
						|
      );
 | 
						|
      this.splitBox.setState({
 | 
						|
        width: this.useLandscapeMode()
 | 
						|
          ? this.sidebarSplitBoxRef.current.state.width
 | 
						|
          : splitterBox.clientWidth,
 | 
						|
      });
 | 
						|
 | 
						|
      // Hide the splitter to prevent any drag events in the sidebar split box and
 | 
						|
      // specify that the end (right panel in horziontal mode or bottom panel in vertical
 | 
						|
      // mode) panel should be uncontrolled when resizing.
 | 
						|
      this.sidebarSplitBoxRef.current.setState({
 | 
						|
        endPanelControl: false,
 | 
						|
        splitterSize: 0,
 | 
						|
      });
 | 
						|
 | 
						|
      this.ruleViewSideBar.hide();
 | 
						|
      this.ruleViewSideBar.removeTab("ruleview");
 | 
						|
 | 
						|
      if (skipQueue) {
 | 
						|
        this.sidebar.addExistingTab(
 | 
						|
          "ruleview",
 | 
						|
          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
 | 
						|
          defaultTab == "ruleview",
 | 
						|
          0
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        this.sidebar.queueExistingTab(
 | 
						|
          "ruleview",
 | 
						|
          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
 | 
						|
          defaultTab == "ruleview",
 | 
						|
          0
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this.emit("ruleview-added");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns a boolean indicating whether a sidebar panel instance exists.
 | 
						|
   */
 | 
						|
  hasPanel: function(id) {
 | 
						|
    return this._panels.has(id);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Lazily get and create panel instances displayed in the sidebar
 | 
						|
   */
 | 
						|
  getPanel: function(id) {
 | 
						|
    if (this._panels.has(id)) {
 | 
						|
      return this._panels.get(id);
 | 
						|
    }
 | 
						|
 | 
						|
    let panel;
 | 
						|
    switch (id) {
 | 
						|
      case "animationinspector":
 | 
						|
        const AnimationInspector = this.browserRequire(
 | 
						|
          "devtools/client/inspector/animation/animation"
 | 
						|
        );
 | 
						|
        panel = new AnimationInspector(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "boxmodel":
 | 
						|
        // box-model isn't a panel on its own, it used to, now it is being used by
 | 
						|
        // the layout view which retrieves an instance via getPanel.
 | 
						|
        const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
 | 
						|
        panel = new BoxModel(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "changesview":
 | 
						|
        const ChangesView = this.browserRequire(
 | 
						|
          "devtools/client/inspector/changes/ChangesView"
 | 
						|
        );
 | 
						|
        panel = new ChangesView(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "compatibilityview":
 | 
						|
        const CompatibilityView = this.browserRequire(
 | 
						|
          "devtools/client/inspector/compatibility/CompatibilityView"
 | 
						|
        );
 | 
						|
        panel = new CompatibilityView(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "computedview":
 | 
						|
        const { ComputedViewTool } = this.browserRequire(
 | 
						|
          "devtools/client/inspector/computed/computed"
 | 
						|
        );
 | 
						|
        panel = new ComputedViewTool(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "fontinspector":
 | 
						|
        const FontInspector = this.browserRequire(
 | 
						|
          "devtools/client/inspector/fonts/fonts"
 | 
						|
        );
 | 
						|
        panel = new FontInspector(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "layoutview":
 | 
						|
        const LayoutView = this.browserRequire(
 | 
						|
          "devtools/client/inspector/layout/layout"
 | 
						|
        );
 | 
						|
        panel = new LayoutView(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      case "ruleview":
 | 
						|
        const {
 | 
						|
          RuleViewTool,
 | 
						|
        } = require("devtools/client/inspector/rules/rules");
 | 
						|
        panel = new RuleViewTool(this, this.panelWin);
 | 
						|
        break;
 | 
						|
      default:
 | 
						|
        // This is a custom panel or a non lazy-loaded one.
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
 | 
						|
    if (panel) {
 | 
						|
      this._panels.set(id, panel);
 | 
						|
    }
 | 
						|
 | 
						|
    return panel;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build the sidebar.
 | 
						|
   */
 | 
						|
  setupSidebar() {
 | 
						|
    const sidebar = this.panelDoc.getElementById("inspector-sidebar");
 | 
						|
    const options = {
 | 
						|
      showAllTabsMenu: true,
 | 
						|
      allTabsMenuButtonTooltip: INSPECTOR_L10N.getStr(
 | 
						|
        "allTabsMenuButton.tooltip"
 | 
						|
      ),
 | 
						|
      sidebarToggleButton: {
 | 
						|
        collapsed: !this.is3PaneModeEnabled,
 | 
						|
        collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
 | 
						|
        expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
 | 
						|
        onClick: this.onSidebarToggle,
 | 
						|
      },
 | 
						|
    };
 | 
						|
 | 
						|
    this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
 | 
						|
    this.sidebar.on("select", this.onSidebarSelect);
 | 
						|
 | 
						|
    const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
 | 
						|
    this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
 | 
						|
      hideTabstripe: true,
 | 
						|
    });
 | 
						|
 | 
						|
    // defaultTab may also be an empty string or a tab id that doesn't exist anymore
 | 
						|
    // (e.g. it was a tab registered by an addon that has been uninstalled).
 | 
						|
    let defaultTab = Services.prefs.getCharPref(
 | 
						|
      "devtools.inspector.activeSidebar"
 | 
						|
    );
 | 
						|
 | 
						|
    if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
 | 
						|
      defaultTab = "layoutview";
 | 
						|
    }
 | 
						|
 | 
						|
    // Append all side panels
 | 
						|
 | 
						|
    this.addRuleView({ defaultTab });
 | 
						|
 | 
						|
    // Inspector sidebar panels in order of appearance.
 | 
						|
    const sidebarPanels = [];
 | 
						|
    sidebarPanels.push({
 | 
						|
      id: "layoutview",
 | 
						|
      title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
 | 
						|
    });
 | 
						|
 | 
						|
    sidebarPanels.push({
 | 
						|
      id: "computedview",
 | 
						|
      title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
 | 
						|
    });
 | 
						|
 | 
						|
    sidebarPanels.push({
 | 
						|
      id: "changesview",
 | 
						|
      title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
 | 
						|
    });
 | 
						|
 | 
						|
    if (
 | 
						|
      Services.prefs.getBoolPref("devtools.inspector.compatibility.enabled")
 | 
						|
    ) {
 | 
						|
      sidebarPanels.push({
 | 
						|
        id: "compatibilityview",
 | 
						|
        title: INSPECTOR_L10N.getStr(
 | 
						|
          "inspector.sidebar.compatibilityViewTitle"
 | 
						|
        ),
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    sidebarPanels.push({
 | 
						|
      id: "fontinspector",
 | 
						|
      title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
 | 
						|
    });
 | 
						|
 | 
						|
    sidebarPanels.push({
 | 
						|
      id: "animationinspector",
 | 
						|
      title: INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
 | 
						|
    });
 | 
						|
 | 
						|
    for (const { id, title } of sidebarPanels) {
 | 
						|
      // The Computed panel is not a React-based panel. We pick its element container from
 | 
						|
      // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
 | 
						|
      // other panels when using the Inspector's tool sidebar.
 | 
						|
      if (id === "computedview") {
 | 
						|
        this.sidebar.queueExistingTab(id, title, defaultTab === id);
 | 
						|
      } else {
 | 
						|
        // When `panel` is a function, it is called when the tab should render. It is
 | 
						|
        // expected to return a React component to populate the tab's content area.
 | 
						|
        // Calling this method on-demand allows us to lazy-load the requested panel.
 | 
						|
        this.sidebar.queueTab(
 | 
						|
          id,
 | 
						|
          title,
 | 
						|
          {
 | 
						|
            props: {
 | 
						|
              id,
 | 
						|
              title,
 | 
						|
            },
 | 
						|
            panel: () => {
 | 
						|
              return this.getPanel(id).provider;
 | 
						|
            },
 | 
						|
          },
 | 
						|
          defaultTab === id
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this.sidebar.addAllQueuedTabs();
 | 
						|
 | 
						|
    // Persist splitter state in preferences.
 | 
						|
    this.sidebar.on("show", this.onSidebarShown);
 | 
						|
    this.sidebar.on("hide", this.onSidebarHidden);
 | 
						|
    this.sidebar.on("destroy", this.onSidebarHidden);
 | 
						|
 | 
						|
    this.sidebar.show();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Setup any extension sidebar already registered to the toolbox when the inspector.
 | 
						|
   * has been created for the first time.
 | 
						|
   */
 | 
						|
  setupExtensionSidebars() {
 | 
						|
    for (const [sidebarId, { title }] of this.toolbox
 | 
						|
      .inspectorExtensionSidebars) {
 | 
						|
      this.addExtensionSidebar(sidebarId, { title });
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create a side-panel tab controlled by an extension
 | 
						|
   * using the devtools.panels.elements.createSidebarPane and sidebar object API
 | 
						|
   *
 | 
						|
   * @param {String} id
 | 
						|
   *        An unique id for the sidebar tab.
 | 
						|
   * @param {Object} options
 | 
						|
   * @param {String} options.title
 | 
						|
   *        The tab title
 | 
						|
   */
 | 
						|
  addExtensionSidebar: function(id, { title }) {
 | 
						|
    if (this._panels.has(id)) {
 | 
						|
      throw new Error(
 | 
						|
        `Cannot create an extension sidebar for the existent id: ${id}`
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const extensionSidebar = new ExtensionSidebar(this, { id, title });
 | 
						|
 | 
						|
    // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
 | 
						|
    // the render of the extension title (e.g. use the icon in the sidebar and show the
 | 
						|
    // extension name in a tooltip).
 | 
						|
    this.addSidebarTab(id, title, extensionSidebar.provider, false);
 | 
						|
 | 
						|
    this._panels.set(id, extensionSidebar);
 | 
						|
 | 
						|
    // Emit the created ExtensionSidebar instance to the listeners registered
 | 
						|
    // on the toolbox by the "devtools.panels.elements" WebExtensions API.
 | 
						|
    this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
 | 
						|
   * extension has been disable/uninstalled while the toolbox and inspector were
 | 
						|
   * still open).
 | 
						|
   *
 | 
						|
   * @param {String} id
 | 
						|
   *        The id of the sidebar tab to destroy.
 | 
						|
   */
 | 
						|
  removeExtensionSidebar: function(id) {
 | 
						|
    if (!this._panels.has(id)) {
 | 
						|
      throw new Error(`Unable to find a sidebar panel with id "${id}"`);
 | 
						|
    }
 | 
						|
 | 
						|
    const panel = this._panels.get(id);
 | 
						|
 | 
						|
    if (!(panel instanceof ExtensionSidebar)) {
 | 
						|
      throw new Error(
 | 
						|
        `The sidebar panel with id "${id}" is not an ExtensionSidebar`
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    this._panels.delete(id);
 | 
						|
    this.sidebar.removeTab(id);
 | 
						|
    panel.destroy();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Register a side-panel tab. This API can be used outside of
 | 
						|
   * DevTools (e.g. from an extension) as well as by DevTools
 | 
						|
   * code base.
 | 
						|
   *
 | 
						|
   * @param {string} tab uniq id
 | 
						|
   * @param {string} title tab title
 | 
						|
   * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
 | 
						|
   * @param {boolean} selected true if the panel should be selected
 | 
						|
   */
 | 
						|
  addSidebarTab: function(id, title, panel, selected) {
 | 
						|
    this.sidebar.addTab(id, title, panel, selected);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Method to check whether the document is a HTML document and
 | 
						|
   * pickColorFromPage method is available or not.
 | 
						|
   *
 | 
						|
   * @return {Boolean} true if the eyedropper highlighter is supported by the current
 | 
						|
   *         document.
 | 
						|
   */
 | 
						|
  async supportsEyeDropper() {
 | 
						|
    try {
 | 
						|
      return await this.inspectorFront.supportsHighlighters();
 | 
						|
    } catch (e) {
 | 
						|
      console.error(e);
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  async setupToolbar() {
 | 
						|
    this.teardownToolbar();
 | 
						|
 | 
						|
    // Setup the add-node button.
 | 
						|
    this.addNode = this.addNode.bind(this);
 | 
						|
    this.addNodeButton = this.panelDoc.getElementById(
 | 
						|
      "inspector-element-add-button"
 | 
						|
    );
 | 
						|
    this.addNodeButton.addEventListener("click", this.addNode);
 | 
						|
 | 
						|
    // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
 | 
						|
    const canShowEyeDropper = await this.supportsEyeDropper();
 | 
						|
 | 
						|
    // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
 | 
						|
    // available.
 | 
						|
    if (!this.panelDoc) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (canShowEyeDropper) {
 | 
						|
      this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
 | 
						|
      this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(
 | 
						|
        this
 | 
						|
      );
 | 
						|
      this.eyeDropperButton = this.panelDoc.getElementById(
 | 
						|
        "inspector-eyedropper-toggle"
 | 
						|
      );
 | 
						|
      this.eyeDropperButton.disabled = false;
 | 
						|
      this.eyeDropperButton.title = INSPECTOR_L10N.getStr(
 | 
						|
        "inspector.eyedropper.label"
 | 
						|
      );
 | 
						|
      this.eyeDropperButton.addEventListener(
 | 
						|
        "click",
 | 
						|
        this.onEyeDropperButtonClicked
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      const eyeDropperButton = this.panelDoc.getElementById(
 | 
						|
        "inspector-eyedropper-toggle"
 | 
						|
      );
 | 
						|
      eyeDropperButton.disabled = true;
 | 
						|
      eyeDropperButton.title = INSPECTOR_L10N.getStr(
 | 
						|
        "eyedropper.disabled.title"
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    this.emit("inspector-toolbar-updated");
 | 
						|
  },
 | 
						|
 | 
						|
  teardownToolbar: function() {
 | 
						|
    if (this.addNodeButton) {
 | 
						|
      this.addNodeButton.removeEventListener("click", this.addNode);
 | 
						|
      this.addNodeButton = null;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.eyeDropperButton) {
 | 
						|
      this.eyeDropperButton.removeEventListener(
 | 
						|
        "click",
 | 
						|
        this.onEyeDropperButtonClicked
 | 
						|
      );
 | 
						|
      this.eyeDropperButton = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _selectionCssSelectors: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set the array of CSS selectors for the currently selected node.
 | 
						|
   * We use an array of selectors in case the element is in iframes.
 | 
						|
   * Will store the current target url along with it to allow pre-selection at
 | 
						|
   * reload
 | 
						|
   */
 | 
						|
  set selectionCssSelectors(cssSelectors = []) {
 | 
						|
    if (this._destroyed) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._selectionCssSelectors = {
 | 
						|
      selectors: cssSelectors,
 | 
						|
      url: this.currentTarget.url,
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the CSS selectors for the current selection if any, that is, if a node
 | 
						|
   * is actually selected and that node has been selected while on the same url
 | 
						|
   */
 | 
						|
  get selectionCssSelectors() {
 | 
						|
    if (
 | 
						|
      this._selectionCssSelectors &&
 | 
						|
      this._selectionCssSelectors.url === this.currentTarget.url
 | 
						|
    ) {
 | 
						|
      return this._selectionCssSelectors.selectors;
 | 
						|
    }
 | 
						|
    return [];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Some inspector ruleview helpers rely on the selectionCssSelector to get the
 | 
						|
   * unique CSS selector of the selected element only within its host document,
 | 
						|
   * disregarding ancestor iframes.
 | 
						|
   * They should not care about the complete array of CSS selectors, only
 | 
						|
   * relevant in order to reselect the proper node when reloading pages with
 | 
						|
   * frames.
 | 
						|
   */
 | 
						|
  get selectionCssSelector() {
 | 
						|
    if (this.selectionCssSelectors.length) {
 | 
						|
      return this.selectionCssSelectors[this.selectionCssSelectors.length - 1];
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * On any new selection made by the user, store the array of css selectors
 | 
						|
   * of the selected node so it can be restored after reload of the same page
 | 
						|
   */
 | 
						|
  updateSelectionCssSelectors() {
 | 
						|
    if (this.selection.isElementNode()) {
 | 
						|
      this.selection.nodeFront.getAllSelectors().then(selectors => {
 | 
						|
        this.selectionCssSelectors = selectors;
 | 
						|
      }, this._handleRejectionIfNotDestroyed);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Can a new HTML element be inserted into the currently selected element?
 | 
						|
   * @return {Boolean}
 | 
						|
   */
 | 
						|
  canAddHTMLChild: function() {
 | 
						|
    const selection = this.selection;
 | 
						|
 | 
						|
    // Don't allow to insert an element into these elements. This should only
 | 
						|
    // contain elements where walker.insertAdjacentHTML has no effect.
 | 
						|
    const invalidTagNames = ["html", "iframe"];
 | 
						|
 | 
						|
    return (
 | 
						|
      selection.isHTMLNode() &&
 | 
						|
      selection.isElementNode() &&
 | 
						|
      !selection.isPseudoElementNode() &&
 | 
						|
      !selection.isAnonymousNode() &&
 | 
						|
      !invalidTagNames.includes(selection.nodeFront.nodeName.toLowerCase())
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Update the state of the add button in the toolbar depending on the current selection.
 | 
						|
   */
 | 
						|
  updateAddElementButton() {
 | 
						|
    const btn = this.panelDoc.getElementById("inspector-element-add-button");
 | 
						|
    if (this.canAddHTMLChild()) {
 | 
						|
      btn.removeAttribute("disabled");
 | 
						|
    } else {
 | 
						|
      btn.setAttribute("disabled", "true");
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handler for the "host-changed" event from the toolbox. Resets the inspector
 | 
						|
   * sidebar sizes when the toolbox host type changes.
 | 
						|
   */
 | 
						|
  async onHostChanged() {
 | 
						|
    // Eagerly call our resize handling code to process the fact that we
 | 
						|
    // switched hosts. If we don't do this, we'll wait for resize events + 200ms
 | 
						|
    // to have passed, which causes the old layout to noticeably show up in the
 | 
						|
    // new host, followed by the updated one.
 | 
						|
    await this._onLazyPanelResize();
 | 
						|
    // Note that we may have been destroyed by now, especially in tests, so we
 | 
						|
    // need to check if that's happened before touching anything else.
 | 
						|
    if (!this.currentTarget || !this.is3PaneModeEnabled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // When changing hosts, the toolbox chromeEventHandler might change, for instance when
 | 
						|
    // switching from docked to window hosts. Recreate the searchbox shortcuts.
 | 
						|
    this.searchboxShortcuts.destroy();
 | 
						|
    this.createSearchBoxShortcuts();
 | 
						|
 | 
						|
    this.setSidebarSplitBoxState();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When a new node is selected.
 | 
						|
   */
 | 
						|
  onNewSelection: function(value, reason) {
 | 
						|
    if (reason === "selection-destroy") {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.updateAddElementButton();
 | 
						|
    this.updateSelectionCssSelectors();
 | 
						|
    this.trackReflowsInSelection();
 | 
						|
 | 
						|
    const selfUpdate = this.updating("inspector-panel");
 | 
						|
    executeSoon(() => {
 | 
						|
      try {
 | 
						|
        selfUpdate(this.selection.nodeFront);
 | 
						|
        this.telemetry.scalarAdd(TELEMETRY_SCALAR_NODE_SELECTION_COUNT, 1);
 | 
						|
      } catch (ex) {
 | 
						|
        console.error(ex);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Starts listening for reflows in the targetFront of the currently selected nodeFront.
 | 
						|
   */
 | 
						|
  async trackReflowsInSelection() {
 | 
						|
    this.untrackReflowsInSelection();
 | 
						|
    if (!this.selection.nodeFront) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._destroyed) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      await this.commands.resourceCommand.watchResources(
 | 
						|
        [this.commands.resourceCommand.TYPES.REFLOW],
 | 
						|
        {
 | 
						|
          onAvailable: this.onReflowInSelection,
 | 
						|
        }
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      // it can happen that watchResources fails as the client closes while we're processing
 | 
						|
      // some asynchronous call.
 | 
						|
      // In order to still get valid exceptions, we re-throw the exception if the inspector
 | 
						|
      // isn't destroyed.
 | 
						|
      if (!this._destroyed) {
 | 
						|
        throw e;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stops listening for reflows.
 | 
						|
   */
 | 
						|
  untrackReflowsInSelection() {
 | 
						|
    this.commands.resourceCommand.unwatchResources(
 | 
						|
      [this.commands.resourceCommand.TYPES.REFLOW],
 | 
						|
      {
 | 
						|
        onAvailable: this.onReflowInSelection,
 | 
						|
      }
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  onReflowInSelection() {
 | 
						|
    // This event will be fired whenever a reflow is detected in the target front of the
 | 
						|
    // selected node front (so when a reflow is detected inside any of the windows that
 | 
						|
    // belong to the BrowsingContext when the currently selected node lives).
 | 
						|
    this.emit("reflow-in-selected-target");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Delay the "inspector-updated" notification while a tool
 | 
						|
   * is updating itself.  Returns a function that must be
 | 
						|
   * invoked when the tool is done updating with the node
 | 
						|
   * that the tool is viewing.
 | 
						|
   */
 | 
						|
  updating: function(name) {
 | 
						|
    if (
 | 
						|
      this._updateProgress &&
 | 
						|
      this._updateProgress.node != this.selection.nodeFront
 | 
						|
    ) {
 | 
						|
      this.cancelUpdate();
 | 
						|
    }
 | 
						|
 | 
						|
    if (!this._updateProgress) {
 | 
						|
      // Start an update in progress.
 | 
						|
      const self = this;
 | 
						|
      this._updateProgress = {
 | 
						|
        node: this.selection.nodeFront,
 | 
						|
        outstanding: new Set(),
 | 
						|
        checkDone: function() {
 | 
						|
          if (this !== self._updateProgress) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          // Cancel update if there is no `selection` anymore.
 | 
						|
          // It can happen if the inspector panel is already destroyed.
 | 
						|
          if (!self.selection || this.node !== self.selection.nodeFront) {
 | 
						|
            self.cancelUpdate();
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          if (this.outstanding.size !== 0) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          self._updateProgress = null;
 | 
						|
          self.emit("inspector-updated", name);
 | 
						|
        },
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    const progress = this._updateProgress;
 | 
						|
    const done = function() {
 | 
						|
      progress.outstanding.delete(done);
 | 
						|
      progress.checkDone();
 | 
						|
    };
 | 
						|
    progress.outstanding.add(done);
 | 
						|
    return done;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Cancel notification of inspector updates.
 | 
						|
   */
 | 
						|
  cancelUpdate: function() {
 | 
						|
    this._updateProgress = null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When a node is deleted, select its parent node or the defaultNode if no
 | 
						|
   * parent is found (may happen when deleting an iframe inside which the
 | 
						|
   * node was selected).
 | 
						|
   */
 | 
						|
  onDetached: function(parentNode) {
 | 
						|
    this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
 | 
						|
    const nodeFront = parentNode ? parentNode : this._defaultNode;
 | 
						|
    this.selection.setNodeFront(nodeFront, { reason: "detached" });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Destroy the inspector.
 | 
						|
   */
 | 
						|
  destroy: function() {
 | 
						|
    if (this._destroyed) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._destroyed = true;
 | 
						|
 | 
						|
    this.cancelUpdate();
 | 
						|
 | 
						|
    this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
 | 
						|
    this.selection.off("new-node-front", this.onNewSelection);
 | 
						|
    this.selection.off("detached-front", this.onDetached);
 | 
						|
    this.toolbox.nodePicker.off("picker-node-canceled", this.onPickerCanceled);
 | 
						|
    this.toolbox.nodePicker.off("picker-node-hovered", this.onPickerHovered);
 | 
						|
    this.toolbox.nodePicker.off("picker-node-picked", this.onPickerPicked);
 | 
						|
 | 
						|
    // Destroy the sidebar first as it may unregister stuff
 | 
						|
    // and still use random attributes on inspector and layout panel
 | 
						|
    this.sidebar.destroy();
 | 
						|
    // Unregister sidebar listener *after* destroying it
 | 
						|
    // in order to process its destroy event and save sidebar sizes
 | 
						|
    this.sidebar.off("select", this.onSidebarSelect);
 | 
						|
    this.sidebar.off("show", this.onSidebarShown);
 | 
						|
    this.sidebar.off("hide", this.onSidebarHidden);
 | 
						|
    this.sidebar.off("destroy", this.onSidebarHidden);
 | 
						|
 | 
						|
    for (const [, panel] of this._panels) {
 | 
						|
      panel.destroy();
 | 
						|
    }
 | 
						|
    this._panels.clear();
 | 
						|
 | 
						|
    if (this._highlighters) {
 | 
						|
      this._highlighters.destroy();
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._search) {
 | 
						|
      this._search.destroy();
 | 
						|
      this._search = null;
 | 
						|
    }
 | 
						|
 | 
						|
    this.ruleViewSideBar.destroy();
 | 
						|
    this.ruleViewSideBar = null;
 | 
						|
 | 
						|
    this._destroyMarkup();
 | 
						|
 | 
						|
    this.teardownToolbar();
 | 
						|
 | 
						|
    this.breadcrumbs.destroy();
 | 
						|
    this.styleChangeTracker.destroy();
 | 
						|
    this.searchboxShortcuts.destroy();
 | 
						|
    this.searchboxShortcuts = null;
 | 
						|
 | 
						|
    this.commands.targetCommand.unwatchTargets(
 | 
						|
      [this.commands.targetCommand.TYPES.FRAME],
 | 
						|
      this._onTargetAvailable,
 | 
						|
      this._onTargetDestroyed
 | 
						|
    );
 | 
						|
    const { resourceCommand } = this.toolbox;
 | 
						|
    resourceCommand.unwatchResources(
 | 
						|
      [
 | 
						|
        resourceCommand.TYPES.ROOT_NODE,
 | 
						|
        resourceCommand.TYPES.CSS_CHANGE,
 | 
						|
        resourceCommand.TYPES.DOCUMENT_EVENT,
 | 
						|
      ],
 | 
						|
      { onAvailable: this.onResourceAvailable }
 | 
						|
    );
 | 
						|
    this.untrackReflowsInSelection();
 | 
						|
 | 
						|
    this._InspectorTabPanel = null;
 | 
						|
    this._TabBar = null;
 | 
						|
    this._InspectorSplitBox = null;
 | 
						|
    this.sidebarSplitBoxRef = null;
 | 
						|
    // Note that we do not unmount inspector-splitter-box
 | 
						|
    // as it regresses inspector closing performance while not releasing
 | 
						|
    // any object (bug 1729925)
 | 
						|
    this.splitBox = null;
 | 
						|
 | 
						|
    this._is3PaneModeChromeEnabled = null;
 | 
						|
    this._is3PaneModeEnabled = null;
 | 
						|
    this._markupBox = null;
 | 
						|
    this._markupFrame = null;
 | 
						|
    this._toolbox = null;
 | 
						|
    this._commands = null;
 | 
						|
    this.breadcrumbs = null;
 | 
						|
    this.inspectorFront = null;
 | 
						|
    this._cssProperties = null;
 | 
						|
    this.accessibilityFront = null;
 | 
						|
    this._highlighters = null;
 | 
						|
    this.walker = null;
 | 
						|
    this._defaultNode = null;
 | 
						|
    this.panelDoc = null;
 | 
						|
    this.panelWin.inspector = null;
 | 
						|
    this.panelWin = null;
 | 
						|
    this.resultsLength = null;
 | 
						|
    this.searchBox.removeEventListener("focus", this.listenForSearchEvents);
 | 
						|
    this.searchBox = null;
 | 
						|
    this.show3PaneTooltip = null;
 | 
						|
    this.sidebar = null;
 | 
						|
    this.store = null;
 | 
						|
    this.telemetry = null;
 | 
						|
  },
 | 
						|
 | 
						|
  _destroyMarkup: function() {
 | 
						|
    if (this.markup) {
 | 
						|
      this.markup.destroy();
 | 
						|
      this.markup = null;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._markupBox) {
 | 
						|
      this._markupBox.style.visibility = "hidden";
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onEyeDropperButtonClicked: function() {
 | 
						|
    this.eyeDropperButton.classList.contains("checked")
 | 
						|
      ? this.hideEyeDropper()
 | 
						|
      : this.showEyeDropper();
 | 
						|
  },
 | 
						|
 | 
						|
  startEyeDropperListeners: function() {
 | 
						|
    this.toolbox.tellRDMAboutPickerState(true, PICKER_TYPES.EYEDROPPER);
 | 
						|
    this.inspectorFront.once("color-pick-canceled", this.onEyeDropperDone);
 | 
						|
    this.inspectorFront.once("color-picked", this.onEyeDropperDone);
 | 
						|
    this.once("new-root", this.onEyeDropperDone);
 | 
						|
  },
 | 
						|
 | 
						|
  stopEyeDropperListeners: function() {
 | 
						|
    this.toolbox.tellRDMAboutPickerState(false, PICKER_TYPES.EYEDROPPER);
 | 
						|
    this.inspectorFront.off("color-pick-canceled", this.onEyeDropperDone);
 | 
						|
    this.inspectorFront.off("color-picked", this.onEyeDropperDone);
 | 
						|
    this.off("new-root", this.onEyeDropperDone);
 | 
						|
  },
 | 
						|
 | 
						|
  onEyeDropperDone: function() {
 | 
						|
    this.eyeDropperButton.classList.remove("checked");
 | 
						|
    this.stopEyeDropperListeners();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Show the eyedropper on the page.
 | 
						|
   * @return {Promise} resolves when the eyedropper is visible.
 | 
						|
   */
 | 
						|
  showEyeDropper: function() {
 | 
						|
    // The eyedropper button doesn't exist, most probably because the actor doesn't
 | 
						|
    // support the pickColorFromPage, or because the page isn't HTML.
 | 
						|
    if (!this.eyeDropperButton) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    // turn off node picker when color picker is starting
 | 
						|
    this.toolbox.nodePicker.stop().catch(console.error);
 | 
						|
    this.telemetry.scalarSet(TELEMETRY_EYEDROPPER_OPENED, 1);
 | 
						|
    this.eyeDropperButton.classList.add("checked");
 | 
						|
    this.startEyeDropperListeners();
 | 
						|
    return this.inspectorFront
 | 
						|
      .pickColorFromPage({ copyOnSelect: true })
 | 
						|
      .catch(console.error);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Hide the eyedropper.
 | 
						|
   * @return {Promise} resolves when the eyedropper is hidden.
 | 
						|
   */
 | 
						|
  hideEyeDropper: function() {
 | 
						|
    // The eyedropper button doesn't exist, most probably  because the page isn't HTML.
 | 
						|
    if (!this.eyeDropperButton) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    this.eyeDropperButton.classList.remove("checked");
 | 
						|
    this.stopEyeDropperListeners();
 | 
						|
    return this.inspectorFront.cancelPickColorFromPage().catch(console.error);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create a new node as the last child of the current selection, expand the
 | 
						|
   * parent and select the new node.
 | 
						|
   */
 | 
						|
  async addNode() {
 | 
						|
    if (!this.canAddHTMLChild()) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // turn off node picker when add node is triggered
 | 
						|
    this.toolbox.nodePicker.stop();
 | 
						|
 | 
						|
    // turn off color picker when add node is triggered
 | 
						|
    this.hideEyeDropper();
 | 
						|
 | 
						|
    const nodeFront = this.selection.nodeFront;
 | 
						|
    const html = "<div></div>";
 | 
						|
 | 
						|
    // Insert the html and expect a childList markup mutation.
 | 
						|
    const onMutations = this.once("markupmutation");
 | 
						|
    await nodeFront.walkerFront.insertAdjacentHTML(
 | 
						|
      this.selection.nodeFront,
 | 
						|
      "beforeEnd",
 | 
						|
      html
 | 
						|
    );
 | 
						|
    await onMutations;
 | 
						|
 | 
						|
    // Expand the parent node.
 | 
						|
    this.markup.expandNode(nodeFront);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Toggle a pseudo class.
 | 
						|
   */
 | 
						|
  togglePseudoClass: function(pseudo) {
 | 
						|
    if (this.selection.isElementNode()) {
 | 
						|
      const node = this.selection.nodeFront;
 | 
						|
      if (node.hasPseudoClassLock(pseudo)) {
 | 
						|
        return node.walkerFront.removePseudoClassLock(node, pseudo, {
 | 
						|
          parents: true,
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      const hierarchical = pseudo == ":hover" || pseudo == ":active";
 | 
						|
      return node.walkerFront.addPseudoClassLock(node, pseudo, {
 | 
						|
        parents: hierarchical,
 | 
						|
      });
 | 
						|
    }
 | 
						|
    return promise.resolve();
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initiate screenshot command on selected node.
 | 
						|
   */
 | 
						|
  async screenshotNode() {
 | 
						|
    // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
 | 
						|
    // is still visible, therefore showing it in the picture.
 | 
						|
    // Note that other highlighters will still be visible. See Bug 1663881
 | 
						|
    await this.highlighters.hideHighlighterType(
 | 
						|
      this.highlighters.TYPES.BOXMODEL
 | 
						|
    );
 | 
						|
 | 
						|
    const clipboardEnabled = Services.prefs.getBoolPref(
 | 
						|
      "devtools.screenshot.clipboard.enabled"
 | 
						|
    );
 | 
						|
    const args = {
 | 
						|
      file: !clipboardEnabled,
 | 
						|
      nodeActorID: this.selection.nodeFront.actorID,
 | 
						|
      clipboard: clipboardEnabled,
 | 
						|
    };
 | 
						|
 | 
						|
    const messages = await captureAndSaveScreenshot(
 | 
						|
      this.selection.nodeFront.targetFront,
 | 
						|
      this.panelWin,
 | 
						|
      args
 | 
						|
    );
 | 
						|
    const notificationBox = this.toolbox.getNotificationBox();
 | 
						|
    const priorityMap = {
 | 
						|
      error: notificationBox.PRIORITY_CRITICAL_HIGH,
 | 
						|
      warn: notificationBox.PRIORITY_WARNING_HIGH,
 | 
						|
    };
 | 
						|
    for (const { text, level } of messages) {
 | 
						|
      // captureAndSaveScreenshot returns "saved" messages, that indicate where the
 | 
						|
      // screenshot was saved. We don't want to display them as the download UI can be
 | 
						|
      // used to open the file.
 | 
						|
      if (level !== "warn" && level !== "error") {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      notificationBox.appendNotification(text, null, null, priorityMap[level]);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an object containing the shared handler functions used in React components.
 | 
						|
   */
 | 
						|
  getCommonComponentProps() {
 | 
						|
    return {
 | 
						|
      setSelectedNode: this.selection.setNodeFront,
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  onPickerCanceled() {
 | 
						|
    this.highlighters.hideHighlighterType(this.highlighters.TYPES.BOXMODEL);
 | 
						|
  },
 | 
						|
 | 
						|
  onPickerHovered(nodeFront) {
 | 
						|
    this.highlighters.showHighlighterTypeForNode(
 | 
						|
      this.highlighters.TYPES.BOXMODEL,
 | 
						|
      nodeFront
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  onPickerPicked(nodeFront) {
 | 
						|
    this.highlighters.showHighlighterTypeForNode(
 | 
						|
      this.highlighters.TYPES.BOXMODEL,
 | 
						|
      nodeFront,
 | 
						|
      { duration: this.HIGHLIGHTER_AUTOHIDE_TIMER }
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async inspectNodeActor(nodeGrip, reason) {
 | 
						|
    const nodeFront = await this.inspectorFront.getNodeFrontFromNodeGrip(
 | 
						|
      nodeGrip
 | 
						|
    );
 | 
						|
    if (!nodeFront) {
 | 
						|
      console.error(
 | 
						|
        "The object cannot be linked to the inspector, the " +
 | 
						|
          "corresponding nodeFront could not be found."
 | 
						|
      );
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const isAttached = await this.walker.isInDOMTree(nodeFront);
 | 
						|
    if (!isAttached) {
 | 
						|
      console.error("Selected DOMNode is not attached to the document tree.");
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.selection.setNodeFront(nodeFront, { reason });
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
exports.Inspector = Inspector;
 |