Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/devtools/client/inspector/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 62 kB image not shown  

Quelle  inspector.js   Sprache: JAVA

 
/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
const flags = require("resource://devtools/shared/flags.js");
const { executeSoon } = require("resource://devtools/shared/DevToolsUtils.js");
const { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
const createStore = require("resource://devtools/client/inspector/store.js");
const InspectorStyleChangeTracker = require("resource://devtools/client/inspector/shared/style-change-tracker.js");
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");

// 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",
  "resource://devtools/client/inspector/breadcrumbs.js",
  true
);
loader.lazyRequireGetter(
  this,
  "KeyShortcuts",
  "resource://devtools/client/shared/key-shortcuts.js"
);
loader.lazyRequireGetter(
  this,
  "InspectorSearch",
  "resource://devtools/client/inspector/inspector-search.js",
  true
);
loader.lazyRequireGetter(
  this,
  "ToolSidebar",
  "resource://devtools/client/inspector/toolsidebar.js",
  true
);
loader.lazyRequireGetter(
  this,
  "MarkupView",
  "resource://devtools/client/inspector/markup/markup.js"
);
loader.lazyRequireGetter(
  this,
  "HighlightersOverlay",
  "resource://devtools/client/inspector/shared/highlighters-overlay.js"
);
loader.lazyRequireGetter(
  this,
  "ExtensionSidebar",
  "resource://devtools/client/inspector/extensions/extension-sidebar.js"
);
loader.lazyRequireGetter(
  this,
  "PICKER_TYPES",
  "resource://devtools/shared/picker-constants.js"
);
loader.lazyRequireGetter(
  this,
  "captureAndSaveScreenshot",
  "resource://devtools/client/shared/screenshot.js",
  true
);
loader.lazyRequireGetter(
  this,
  "debounce",
  "resource://devtools/shared/debounce.js",
  true
);

const {
  LocalizationHelper,
  localizeMarkup,
} = require("resource://devtools/shared/l10n.js");
const INSPECTOR_L10N = new LocalizationHelper(
  "devtools/client/locales/inspector.properties"
);
const {
  FluentL10n,
} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");

// 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_CHROME_ENABLED_PREF =
  "devtools.inspector.chrome.three-pane-enabled";
const DEFAULT_COLOR_UNIT_PREF = "devtools.defaultColorUnit";

/**
 * 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._handleDefaultColorUnitPrefChange =
    this._handleDefaultColorUnitPrefChange.bind(this);
  this._handleRejectionIfNotDestroyed =
    this._handleRejectionIfNotDestroyed.bind(this);
  this._onTargetAvailable = this._onTargetAvailable.bind(this);
  this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
  this._onTargetSelected = this._onTargetSelected.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._onLazyPanelResize = this._onLazyPanelResize.bind(this);
  this.onPanelWindowResize = debounce(
    this._onLazyPanelResize,
    LAZY_RESIZE_INTERVAL_MS,
    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);

  this.prefObserver = new PrefObserver("devtools.");
  this.prefObserver.on(
    DEFAULT_COLOR_UNIT_PREF,
    this._handleDefaultColorUnitPrefChange
  );
  this.defaultColorUnit = Services.prefs.getStringPref(DEFAULT_COLOR_UNIT_PREF);
}

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({
      types: [this.commands.targetCommand.TYPES.FRAME],
      onAvailable: this._onTargetAvailable,
      onSelected: this._onTargetSelected,
      onDestroyed: this._onTargetDestroyed,
    });

    const { TYPES } = this.toolbox.resourceCommand;
    this._watchedResources = [
      // To observe CSS change before opening changes view.
      TYPES.CSS_CHANGE,
      TYPES.DOCUMENT_EVENT,
    ];
    // The root node is retrieved from onTargetSelected which is now called
    // on startup as well as on any navigation (= new top level target).
    //
    // We only listen to new root node in the browser toolbox, which is the last
    // configuration to use one target for multiple window global.
    const isBrowserToolbox =
      this.commands.descriptorFront.isBrowserProcessDescriptor;
    if (isBrowserToolbox) {
      this._watchedResources.push(TYPES.ROOT_NODE);
    }

    await this.toolbox.resourceCommand.watchResources(this._watchedResources, {
      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?"
    Glean.devtoolsInspector.threePaneEnabled[this.is3PaneModeEnabled].add(1);

    return this;
  },

  // The onTargetAvailable argument is mandatory for TargetCommand.watchTargets.
  // The inspector ignore all targets but the currently selected one,
  // so all the target work is done from onTargetSelected.
  async _onTargetAvailable({ targetFront }) {
    if (!targetFront.isTopLevel) {
      return;
    }

    // Fetch data and fronts which aren't WindowGlobal specific
    // and can be fetched once from the top level target.
    await Promise.all([
      this._getCssProperties(targetFront),
      this._getAccessibilityFront(targetFront),
    ]);
  },

  async _onTargetSelected({ targetFront }) {
    // We don't use this.highlighters since it creates a HighlightersOverlay if it wasn't
    // the case yet.
    if (this._highlighters) {
      this._highlighters.hideAllHighlighters();
    }
    if (targetFront.isDestroyed()) {
      return;
    }

    await this.initInspectorFront(targetFront);

    // the target might have been destroyed when reloading quickly,
    // while waiting for inspector front initialization
    if (targetFront.isDestroyed()) {
      return;
    }

    const { walker } = await targetFront.getFront("inspector");
    const rootNodeFront = await walker.getRootNode();

    // onRootNodeAvailable will take care of populating the markup view
    await this.onRootNodeAvailable(rootNodeFront);
  },

  _onTargetDestroyed({ targetFront }) {
    // Ignore all targets but the top level one
    if (!targetFront.isTopLevel) {
      return;
    }

    this._defaultNode = null;
    this.selection.setNodeFront(null);
  },

  onResourceAvailable(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.
   */

  async onRootNodeAvailable(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);
    }
  },

  async _initMarkupView() {
    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(thisthis._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";
        this.telemetry.getHistogramById(telemetryKey).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 _3PanePrefName() {
    // All other contexts: webextension and browser toolbox
    // are considered as "chrome"
    return this.commands.descriptorFront.isTabDescriptor
      ? THREE_PANE_ENABLED_PREF
      : THREE_PANE_CHROME_ENABLED_PREF;
  },

  get is3PaneModeEnabled() {
    if (!this._is3PaneModeEnabled) {
      this._is3PaneModeEnabled = Services.prefs.getBoolPref(
        this._3PanePrefName
      );
    }
    return this._is3PaneModeEnabled;
  },

  set is3PaneModeEnabled(value) {
    this._is3PaneModeEnabled = value;
    Services.prefs.setBoolPref(this._3PanePrefName, 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,

  _handleDefaultColorUnitPrefChange() {
    this.defaultColorUnit = Services.prefs.getStringPref(
      DEFAULT_COLOR_UNIT_PREF
    );
  },

  /**
   * 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(e) {
    if (!this._destroyed) {
      console.error(e);
    }
  },

  _onWillNavigate() {
    this._defaultNode = null;
    this.selection.setNodeFront(null);
    if (this._highlighters) {
      this._highlighters.hideAllHighlighters();
    }
    this._destroyMarkup();
    this._pendingSelectionUnique = null;
  },

  async _getCssProperties(targetFront) {
    this._cssProperties = await targetFront.getFront("cssProperties");
  },

  async _getAccessibilityFront(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.
   */

  async _getDefaultNodeForSelection(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 = rootNodeFront.walkerFront;
    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
          ? this.commands.inspectorCommand.findNodeFrontFromSelectors(
              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.selectedTargetFront;
  },

  /**
   * Hooks the searchbar to show result and auto completion suggestions.
   */

  setupSearchBox() {
    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(result) {
    return this._updateSearchResultsLabel(result, true);
  },

  _updateSearchResultsLabel(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() {
    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() {
    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);
  },

  async _onLazyPanelResize() {
    // We can be called on a closed window or destroyed toolbox because of the deferred task.
    if (
      window.closed ||
      this._destroyed ||
      this._toolbox.currentToolId !== "inspector"
    ) {
      return;
    }

    this.splitBox.setState({ vert: this.useLandscapeMode() });
    this.emit("inspector-resize");
  },

  getSidebarSize() {
    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() {
    // 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(width, height) {
    this.toolbox.emit("inspector-sidebar-resized", { width, height });
  },

  /**
   * Returns inspector tab that is active.
   */

  getActiveSidebar() {
    return Services.prefs.getCharPref("devtools.inspector.activeSidebar");
  },

  setActiveSidebar(toolId) {
    Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
  },

  /**
   * Returns tab that is explicitly selected by user.
   */

  getSelectedSidebar() {
    return Services.prefs.getCharPref("devtools.inspector.selectedSidebar");
  },

  setSelectedSidebar(toolId) {
    Services.prefs.setCharPref("devtools.inspector.selectedSidebar", toolId);
  },

  onSidebarSelect(toolId) {
    // Save the currently selected sidebar panel
    this.setSelectedSidebar(toolId);
    this.setActiveSidebar(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() {
    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. Rule view is selected when switching to 2 pane mode. Selected sidebar pref
   * is used otherwise.
   */

  addRuleView({ skipQueue = false } = {}) {
    const selectedSidebar = this.getSelectedSidebar();
    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.sidebar.select(selectedSidebar);

      this.ruleViewSideBar.addExistingTab(
        "ruleview",
        INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
        true
      );

      this.ruleViewSideBar.show();
    } else {
      // When switching to 2 pane view, always set rule view as the active sidebar.
      this.setActiveSidebar("ruleview");
      // 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"),
          true,
          0
        );
      } else {
        this.sidebar.queueExistingTab(
          "ruleview",
          INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
          true,
          0
        );
      }
    }

    // Adding or removing a tab from sidebar sets selectedSidebar by the active tab,
    // which we should revert.
    this.setSelectedSidebar(selectedSidebar);

    this.emit("ruleview-added");
  },

  /**
   * Returns a boolean indicating whether a sidebar panel instance exists.
   */

  hasPanel(id) {
    return this._panels.has(id);
  },

  /**
   * Lazily get and create panel instances displayed in the sidebar
   */

  getPanel(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(thisthis.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("resource://devtools/client/inspector/boxmodel/box-model.js");
        panel = new BoxModel(thisthis.panelWin);
        break;
      case "changesview":
        const ChangesView = this.browserRequire(
          "devtools/client/inspector/changes/ChangesView"
        );
        panel = new ChangesView(thisthis.panelWin);
        break;
      case "compatibilityview":
        const CompatibilityView = this.browserRequire(
          "devtools/client/inspector/compatibility/CompatibilityView"
        );
        panel = new CompatibilityView(thisthis.panelWin);
        break;
      case "computedview":
        const { ComputedViewTool } = this.browserRequire(
          "devtools/client/inspector/computed/computed"
        );
        panel = new ComputedViewTool(thisthis.panelWin);
        break;
      case "fontinspector":
        const FontInspector = this.browserRequire(
          "devtools/client/inspector/fonts/fonts"
        );
        panel = new FontInspector(thisthis.panelWin);
        break;
      case "layoutview":
        const LayoutView = this.browserRequire(
          "devtools/client/inspector/layout/layout"
        );
        panel = new LayoutView(thisthis.panelWin);
        break;
      case "ruleview":
        const {
          RuleViewTool,
        } = require("resource://devtools/client/inspector/rules/rules.js");
        panel = new RuleViewTool(thisthis.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,
    });

    // Append all side panels
    this.addRuleView();

    // 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"),
    });

    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"),
    });

    const defaultTab = this.getActiveSidebar();

    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(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(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(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() {
    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 [];
  },

  /**
   * 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()) {
      return;
    }

    this.commands.inspectorCommand
      .getNodeFrontSelectorsFromTopDocument(this.selection.nodeFront)
      .then(selectors => {
        this.selectionCssSelectors = selectors;
        // emit an event so tests relying on the property being set can properly wait
        // for it.
        this.emitForTests("selection-css-selectors-updated", selectors);
      }, this._handleRejectionIfNotDestroyed);
  },

  /**
   * Can a new HTML element be inserted into the currently selected element?
   * @return {Boolean}
   */

  canAddHTMLChild() {
    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(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);
        Glean.devtoolsInspector.nodeSelectionCount.add(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(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() {
          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() {
    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(parentNode) {
    this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
    const nodeFront = parentNode ? parentNode : this._defaultNode;
    this.selection.setNodeFront(nodeFront, { reason: "detached" });
  },

  /**
   * Destroy the inspector.
   */

  destroy() {
    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.prefObserver.on(
      DEFAULT_COLOR_UNIT_PREF,
      this._handleDefaultColorUnitPrefChange
    );
    this.prefObserver.destroy();

    this.breadcrumbs.destroy();
    this.styleChangeTracker.destroy();
    this.searchboxShortcuts.destroy();
    this.searchboxShortcuts = null;

    this.commands.targetCommand.unwatchTargets({
      types: [this.commands.targetCommand.TYPES.FRAME],
      onAvailable: this._onTargetAvailable,
      onSelected: this._onTargetSelected,
      onDestroyed: this._onTargetDestroyed,
    });
    const { resourceCommand } = this.toolbox;
    resourceCommand.unwatchResources(this._watchedResources, {
      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._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() {
    if (this.markup) {
      this.markup.destroy();
      this.markup = null;
    }

    if (this._markupBox) {
      this._markupBox.style.visibility = "hidden";
    }
  },

  onEyeDropperButtonClicked() {
    this.eyeDropperButton.classList.contains("checked")
      ? this.hideEyeDropper()
      : this.showEyeDropper();
  },

  startEyeDropperListeners() {
    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() {
    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() {
    this.eyeDropperButton.classList.remove("checked");
    this.stopEyeDropperListeners();
  },

  /**
   * Show the eyedropper on the page.
   * @return {Promise} resolves when the eyedropper is visible.
   */

  showEyeDropper() {
    // 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({ canceled: true }).catch(console.error);
    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() {
    // 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({ canceled: true });

    // turn off color picker when add node is triggered
    this.hideEyeDropper();

    const nodeFront = this.selection.nodeFront;
    const html = "
"
;

    // 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(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, nullnull, 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) {
    if (this.toolbox.isDebugTargetFenix()) {
      // When debugging a phone, as we don't have the "hover overlay", we want to provide
      // feedback to the user so they know where they tapped
      this.highlighters.showHighlighterTypeForNode(
        this.highlighters.TYPES.BOXMODEL,
        nodeFront,
        { duration: this.HIGHLIGHTER_AUTOHIDE_TIMER }
      );
      return;
    }
    this.highlighters.hideHighlighterType(this.highlighters.TYPES.BOXMODEL);
  },

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

  /**
   * Called by toolbox.js on `Esc` keydown.
   *
   * @param {AbortController} abortController
   */

  onToolboxChromeEventHandlerEscapeKeyDown(abortController) {
    // If the event tooltip is displayed, hide it and prevent the Esc event listener
    // of the toolbox to occur (e.g. don't toggle split console)
    if (
      this.markup.hasEventDetailsTooltip() &&
      this.markup.eventDetailsTooltip.isVisible()
    ) {
      this.markup.eventDetailsTooltip.hide();
      abortController.abort();
    }
  },
};

exports.Inspector = Inspector;

Messung V0.5
C=88 H=98 G=93

¤ Dauer der Verarbeitung: 0.12 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.