1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* global window, BrowserLoader */
11 const Services = require("Services");
12 const promise = require("promise");
13 const EventEmitter = require("devtools/shared/event-emitter");
14 const { executeSoon } = require("devtools/shared/DevToolsUtils");
15 const { Toolbox } = require("devtools/client/framework/toolbox");
16 const ReflowTracker = require("devtools/client/inspector/shared/reflow-tracker");
17 const Store = require("devtools/client/inspector/store");
18 const InspectorStyleChangeTracker = require("devtools/client/inspector/shared/style-change-tracker");
20 // Use privileged promise in panel documents to prevent having them to freeze
21 // during toolbox destruction. See bug 1402779.
22 const Promise = require("Promise");
24 loader.lazyRequireGetter(
27 "devtools/shared/fronts/css-properties",
30 loader.lazyRequireGetter(
33 "devtools/client/inspector/breadcrumbs",
36 loader.lazyRequireGetter(
39 "devtools/client/shared/key-shortcuts"
41 loader.lazyRequireGetter(
44 "devtools/client/inspector/inspector-search",
47 loader.lazyRequireGetter(
50 "devtools/client/inspector/toolsidebar",
53 loader.lazyRequireGetter(
56 "devtools/client/inspector/markup/markup"
58 loader.lazyRequireGetter(
60 "HighlightersOverlay",
61 "devtools/client/inspector/shared/highlighters-overlay"
63 loader.lazyRequireGetter(
66 "devtools/client/inspector/extensions/extension-sidebar"
68 loader.lazyRequireGetter(
71 "devtools/shared/screenshot/save"
77 "resource://gre/modules/DeferredTask.jsm"
80 const { LocalizationHelper, localizeMarkup } = require("devtools/shared/l10n");
81 const INSPECTOR_L10N = new LocalizationHelper(
82 "devtools/client/locales/inspector.properties"
86 const INITIAL_SIDEBAR_SIZE = 350;
88 // How long we wait to debounce resize events
89 const LAZY_RESIZE_INTERVAL_MS = 200;
91 // If the toolbox's width is smaller than the given amount of pixels, the sidebar
92 // automatically switches from 'landscape/horizontal' to 'portrait/vertical' mode.
93 const PORTRAIT_MODE_WIDTH_THRESHOLD = 700;
94 // If the toolbox's width docked to the side is smaller than the given amount of pixels,
95 // the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
97 const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
99 const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
100 const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
101 const THREE_PANE_CHROME_ENABLED_PREF =
102 "devtools.inspector.chrome.three-pane-enabled";
103 const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
104 const TELEMETRY_SCALAR_NODE_SELECTION_COUNT =
105 "devtools.inspector.node_selection_count";
108 * Represents an open instance of the Inspector for a tab.
109 * The inspector controls the breadcrumbs, the markup view, and the sidebar
110 * (computed view, rule view, font view and animation inspector).
114 * Fired when the inspector panel is opened for the first time and ready to
117 * Fired after a new root (navigation to a new page) event was fired by
118 * the walker, and taken into account by the inspector (after the markup
119 * view has been reloaded)
121 * Fired when the markup-view frame has loaded
122 * - breadcrumbs-updated
123 * Fired when the breadcrumb widget updates to a new node
124 * - boxmodel-view-updated
125 * Fired when the box model updates to a new node
127 * Fired after markup mutations have been processed by the markup-view
128 * - computed-view-refreshed
129 * Fired when the computed rules view updates to a new node
130 * - computed-view-property-expanded
131 * Fired when a property is expanded in the computed rules view
132 * - computed-view-property-collapsed
133 * Fired when a property is collapsed in the computed rules view
134 * - computed-view-sourcelinks-updated
135 * Fired when the stylesheet source links have been updated (when switching
136 * to source-mapped files)
137 * - rule-view-refreshed
138 * Fired when the rule view updates to a new node
139 * - rule-view-sourcelinks-updated
140 * Fired when the stylesheet source links have been updated (when switching
141 * to source-mapped files)
143 function Inspector(toolbox) {
144 EventEmitter.decorate(this);
146 this._toolbox = toolbox;
147 this._target = toolbox.target;
148 this.panelDoc = window.document;
149 this.panelWin = window;
150 this.panelWin.inspector = this;
151 this.telemetry = toolbox.telemetry;
152 this.store = Store();
154 // Map [panel id => panel instance]
155 // Stores all the instances of sidebar panels like rule view, computed view, ...
156 this._panels = new Map();
158 this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
159 this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(
162 this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
163 this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
164 this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
166 this.onDetached = this.onDetached.bind(this);
167 this.onHostChanged = this.onHostChanged.bind(this);
168 this.onMarkupLoaded = this.onMarkupLoaded.bind(this);
169 this.onNewSelection = this.onNewSelection.bind(this);
170 this.onNewRoot = this.onNewRoot.bind(this);
171 this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
172 this.onShowBoxModelHighlighterForNode = this.onShowBoxModelHighlighterForNode.bind(
175 this.onSidebarHidden = this.onSidebarHidden.bind(this);
176 this.onSidebarResized = this.onSidebarResized.bind(this);
177 this.onSidebarSelect = this.onSidebarSelect.bind(this);
178 this.onSidebarShown = this.onSidebarShown.bind(this);
179 this.onSidebarToggle = this.onSidebarToggle.bind(this);
180 this.handleThreadPaused = this.handleThreadPaused.bind(this);
181 this.handleThreadResumed = this.handleThreadResumed.bind(this);
184 Inspector.prototype = {
186 * InspectorPanel.open() is effectively an asynchronous constructor.
187 * Set any attributes or listeners that rely on the document being loaded or fronts
188 * from the InspectorFront and Target here.
191 // Localize all the nodes containing a data-localization attribute.
192 localizeMarkup(this.panelDoc);
194 // When replaying, we need to listen to changes in the target's pause state.
195 if (this.target.isReplayEnabled()) {
196 let dbg = this._toolbox.getPanel("jsdebugger");
198 dbg = await this._toolbox.loadTool("jsdebugger");
200 this._replayResumed = !dbg.isPaused();
202 this.target.threadFront.on("paused", this.handleThreadPaused);
203 this.target.threadFront.on("resumed", this.handleThreadResumed);
206 await this.initInspectorFront();
208 this.target.on("will-navigate", this._onBeforeNavigate);
211 this._getCssProperties(),
212 this._getPageStyle(),
213 this._getDefaultSelection(),
214 this._getAccessibilityFront(),
215 this._getChangesFront(),
218 // Store the URL of the target page prior to navigation in order to ensure
219 // telemetry counts in the Grid Inspector are not double counted on reload.
220 this.previousURL = this.target.url;
221 this.reflowTracker = new ReflowTracker(this.target);
222 this.styleChangeTracker = new InspectorStyleChangeTracker(this);
224 this._markupBox = this.panelDoc.getElementById("markup-box");
226 return this._deferredOpen();
229 async initInspectorFront() {
230 this.inspectorFront = await this.target.getFront("inspector");
231 this.highlighter = this.inspectorFront.highlighter;
232 this.selection = this.inspectorFront.selection;
233 this.walker = this.inspectorFront.walker;
237 return this._toolbox;
241 if (!this._highlighters) {
242 this._highlighters = new HighlightersOverlay(this);
245 return this._highlighters;
248 get isHighlighterReady() {
249 return !!this._highlighters;
252 get is3PaneModeEnabled() {
253 if (this.target.chrome) {
254 if (!this._is3PaneModeChromeEnabled) {
255 this._is3PaneModeChromeEnabled = Services.prefs.getBoolPref(
256 THREE_PANE_CHROME_ENABLED_PREF
260 return this._is3PaneModeChromeEnabled;
263 if (!this._is3PaneModeEnabled) {
264 this._is3PaneModeEnabled = Services.prefs.getBoolPref(
265 THREE_PANE_ENABLED_PREF
269 return this._is3PaneModeEnabled;
272 set is3PaneModeEnabled(value) {
273 if (this.target.chrome) {
274 this._is3PaneModeChromeEnabled = value;
275 Services.prefs.setBoolPref(
276 THREE_PANE_CHROME_ENABLED_PREF,
277 this._is3PaneModeChromeEnabled
280 this._is3PaneModeEnabled = value;
281 Services.prefs.setBoolPref(
282 THREE_PANE_ENABLED_PREF,
283 this._is3PaneModeEnabled
290 this._search = new InspectorSearch(
293 this.searchClearButton
300 get cssProperties() {
301 return this._cssProperties.cssProperties;
305 * Handle promise rejections for various asynchronous actions, and only log errors if
306 * the inspector panel still exists.
307 * This is useful to silence useless errors that happen when the inspector is closed
308 * while still initializing (and making protocol requests).
310 _handleRejectionIfNotDestroyed: function(e) {
311 if (!this._destroyed) {
316 _deferredOpen: async function() {
317 const onMarkupLoaded = this.once("markuploaded");
319 this.isReady = false;
321 // Set the node front so that the markup and sidebar panels will have the selected
322 // nodeFront ready when they're initialized.
323 if (this._defaultNode) {
324 this.selection.setNodeFront(this._defaultNode, {
325 reason: "inspector-open",
329 // Setup the splitter before the sidebar is displayed so, we don't miss any events.
330 this.setupSplitter();
332 // We can display right panel with: tab bar, markup view and breadbrumb. Right after
333 // the splitter set the right and left panel sizes, in order to avoid resizing it
334 // during load of the inspector.
335 this.panelDoc.getElementById("inspector-main-content").style.visibility =
338 // Setup the sidebar panels.
339 await this.setupSidebar();
341 await onMarkupLoaded;
344 // All the components are initialized. Take care of the remaining initialization
346 this.breadcrumbs = new HTMLBreadcrumbs(this);
347 this.setupExtensionSidebars();
348 this.setupSearchBox();
349 await this.setupToolbar();
351 this.onNewSelection();
353 this.walker.on("new-root", this.onNewRoot);
354 this.toolbox.on("host-changed", this.onHostChanged);
355 this.selection.on("new-node-front", this.onNewSelection);
356 this.selection.on("detached-front", this.onDetached);
358 // Log the 3 pane inspector setting on inspector open. The question we want to answer
360 // "What proportion of users use the 3 pane vs 2 pane inspector on inspector open?"
361 this.telemetry.keyedScalarAdd(
362 THREE_PANE_ENABLED_SCALAR,
363 this.is3PaneModeEnabled,
371 _onBeforeNavigate: function() {
372 this._defaultNode = null;
373 this.selection.setNodeFront(null);
374 this._destroyMarkup();
375 this._pendingSelection = null;
378 _getCssProperties: function() {
379 return initCssProperties(this.toolbox).then(cssProperties => {
380 this._cssProperties = cssProperties;
381 }, this._handleRejectionIfNotDestroyed);
384 _getAccessibilityFront: async function() {
385 this.accessibilityFront = await this.target.getFront("accessibility");
386 return this.accessibilityFront;
389 _getChangesFront: async function() {
390 // Get the Changes front, then call a method on it, which will instantiate
391 // the ChangesActor. We want the ChangesActor to be guaranteed available before
392 // the user makes any changes.
393 this.changesFront = await this.toolbox.target.getFront("changes");
394 await this.changesFront.start();
395 return this.changesFront;
398 _getDefaultSelection: function() {
399 // This may throw if the document is still loading and we are
400 // refering to a dead about:blank document
401 return this._getDefaultNodeForSelection().catch(
402 this._handleRejectionIfNotDestroyed
406 _getPageStyle: function() {
407 return this.inspectorFront.getPageStyle().then(pageStyle => {
408 this.pageStyle = pageStyle;
409 }, this._handleRejectionIfNotDestroyed);
413 * Return a promise that will resolve to the default node for selection.
415 _getDefaultNodeForSelection: function() {
416 if (this._defaultNode) {
417 return this._defaultNode;
419 const walker = this.walker;
421 const pendingSelection = this._pendingSelection;
423 // A helper to tell if the target has or is about to navigate.
424 // this._pendingSelection changes on "will-navigate" and "new-root" events.
425 // When replaying, if the target is unpaused then we consider it to be
426 // navigating so that its tree will not be constructed.
427 const hasNavigated = () => {
428 return pendingSelection !== this._pendingSelection || this._replayResumed;
431 // If available, set either the previously selected node or the body
432 // as default selected, else set documentElement
436 if (hasNavigated()) {
437 return promise.reject(
438 "navigated; resolution of _defaultNode aborted"
443 if (this.selectionCssSelector) {
444 return walker.querySelector(rootNode, this.selectionCssSelector);
449 if (hasNavigated()) {
450 return promise.reject(
451 "navigated; resolution of _defaultNode aborted"
458 return walker.querySelector(rootNode, "body");
461 if (hasNavigated()) {
462 return promise.reject(
463 "navigated; resolution of _defaultNode aborted"
470 return this.walker.documentElement();
473 if (hasNavigated()) {
474 return promise.reject(
475 "navigated; resolution of _defaultNode aborted"
478 this._defaultNode = node;
494 this._target = value;
498 * Hooks the searchbar to show result and auto completion suggestions.
500 setupSearchBox: function() {
501 this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
502 this.searchClearButton = this.panelDoc.getElementById(
503 "inspector-searchinput-clear"
505 this.searchResultsContainer = this.panelDoc.getElementById(
506 "inspector-searchlabel-container"
508 this.searchResultsLabel = this.panelDoc.getElementById(
509 "inspector-searchlabel"
512 this.searchBox.addEventListener(
515 this.search.on("search-cleared", this._clearSearchResultsLabel);
516 this.search.on("search-result", this._updateSearchResultsLabel);
521 this.createSearchBoxShortcuts();
524 createSearchBoxShortcuts() {
525 this.searchboxShortcuts = new KeyShortcuts({
526 window: this.panelDoc.defaultView,
527 // The inspector search shortcuts need to be available from everywhere in the
528 // inspector, and the inspector uses iframes (markupview, sidepanel webextensions).
529 // Use the chromeEventHandler as the target to catch events from all frames.
530 target: this.toolbox.getChromeEventHandler(),
532 const key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
533 this.searchboxShortcuts.on(key, event => {
534 // Prevent overriding same shortcut from the computed/rule views
536 event.target.closest("#sidebar-panel-ruleview") ||
537 event.target.closest("#sidebar-panel-computedview")
541 event.preventDefault();
542 this.searchBox.focus();
546 get searchSuggestions() {
547 return this.search.autocompleter;
550 _clearSearchResultsLabel: function(result) {
551 return this._updateSearchResultsLabel(result, true);
554 _updateSearchResultsLabel: function(result, clear = false) {
558 str = INSPECTOR_L10N.getFormatStr(
559 "inspector.searchResultsCount2",
560 result.resultsIndex + 1,
564 str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
567 this.searchResultsContainer.hidden = false;
569 this.searchResultsContainer.hidden = true;
572 this.searchResultsLabel.textContent = str;
576 return this._toolbox.React;
580 return this._toolbox.ReactDOM;
584 return this._toolbox.ReactRedux;
587 get browserRequire() {
588 return this._toolbox.browserRequire;
591 get InspectorTabPanel() {
592 if (!this._InspectorTabPanel) {
593 this._InspectorTabPanel = this.React.createFactory(
595 "devtools/client/inspector/components/InspectorTabPanel"
599 return this._InspectorTabPanel;
602 get InspectorSplitBox() {
603 if (!this._InspectorSplitBox) {
604 this._InspectorSplitBox = this.React.createFactory(
606 "devtools/client/shared/components/splitter/SplitBox"
610 return this._InspectorSplitBox;
615 this._TabBar = this.React.createFactory(
616 this.browserRequire("devtools/client/shared/components/tabs/TabBar")
623 * Check if the inspector should use the landscape mode.
625 * @return {Boolean} true if the inspector should be in landscape mode.
627 useLandscapeMode: function() {
628 if (!this.panelDoc) {
632 const splitterBox = this.panelDoc.getElementById("inspector-splitter-box");
633 const { width } = window.windowUtils.getBoundsWithoutFlushing(splitterBox);
635 return this.is3PaneModeEnabled &&
636 (this.toolbox.hostType == Toolbox.HostType.LEFT ||
637 this.toolbox.hostType == Toolbox.HostType.RIGHT)
638 ? width > SIDE_PORTAIT_MODE_WIDTH_THRESHOLD
639 : width > PORTRAIT_MODE_WIDTH_THRESHOLD;
643 * Build Splitter located between the main and side area of
644 * the Inspector panel.
646 setupSplitter: function() {
647 const { width, height, splitSidebarWidth } = this.getSidebarSize();
649 this.sidebarSplitBoxRef = this.React.createRef();
651 const splitter = this.InspectorSplitBox({
652 className: "inspector-sidebar-splitter",
654 initialHeight: height,
658 endPanelControl: true,
659 startPanel: this.InspectorTabPanel({
660 id: "inspector-main-content",
662 endPanel: this.InspectorSplitBox({
663 initialWidth: splitSidebarWidth,
666 splitterSize: this.is3PaneModeEnabled ? 1 : 0,
667 endPanelControl: this.is3PaneModeEnabled,
668 startPanel: this.InspectorTabPanel({
669 id: "inspector-rules-container",
671 endPanel: this.InspectorTabPanel({
672 id: "inspector-sidebar-container",
674 ref: this.sidebarSplitBoxRef,
676 vert: this.useLandscapeMode(),
677 onControlledPanelResized: this.onSidebarResized,
680 this.splitBox = this.ReactDOM.render(
682 this.panelDoc.getElementById("inspector-splitter-box")
685 this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
688 _onLazyPanelResize: async function() {
689 // We can be called on a closed window because of the deferred task.
694 this.splitBox.setState({ vert: this.useLandscapeMode() });
695 this.emit("inspector-resize");
699 * If Toolbox width is less than 600 px, the splitter changes its mode
700 * to `horizontal` to support portrait view.
702 onPanelWindowResize: function() {
703 if (this.toolbox.currentToolId !== "inspector") {
707 if (!this._lazyResizeHandler) {
708 this._lazyResizeHandler = new DeferredTask(
709 this._onLazyPanelResize.bind(this),
710 LAZY_RESIZE_INTERVAL_MS,
714 this._lazyResizeHandler.arm();
717 getSidebarSize: function() {
720 let splitSidebarWidth;
722 // Initialize splitter size from preferences.
724 width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
725 height = Services.prefs.getIntPref(
726 "devtools.toolsidebar-height.inspector"
728 splitSidebarWidth = Services.prefs.getIntPref(
729 "devtools.toolsidebar-width.inspector.splitsidebar"
732 // Set width and height of the splitter. Only one
733 // value is really useful at a time depending on the current
734 // orientation (vertical/horizontal).
735 // Having both is supported by the splitter component.
736 width = this.is3PaneModeEnabled
737 ? INITIAL_SIDEBAR_SIZE * 2
738 : INITIAL_SIDEBAR_SIZE;
739 height = INITIAL_SIDEBAR_SIZE;
740 splitSidebarWidth = INITIAL_SIDEBAR_SIZE;
743 return { width, height, splitSidebarWidth };
746 onSidebarHidden: function() {
747 // Store the current splitter size to preferences.
748 const state = this.splitBox.state;
749 Services.prefs.setIntPref(
750 "devtools.toolsidebar-width.inspector",
753 Services.prefs.setIntPref(
754 "devtools.toolsidebar-height.inspector",
757 Services.prefs.setIntPref(
758 "devtools.toolsidebar-width.inspector.splitsidebar",
759 this.sidebarSplitBoxRef.current.state.width
763 onSidebarResized: function(width, height) {
764 this.toolbox.emit("inspector-sidebar-resized", { width, height });
767 onSidebarSelect: function(toolId) {
768 // Save the currently selected sidebar panel
769 Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
771 // Then forces the panel creation by calling getPanel
772 // (This allows lazy loading the panels only once we select them)
773 this.getPanel(toolId);
775 this.toolbox.emit("inspector-sidebar-select", toolId);
778 onSidebarShown: function() {
779 const { width, height, splitSidebarWidth } = this.getSidebarSize();
780 this.splitBox.setState({ width, height });
781 this.sidebarSplitBoxRef.current.setState({ width: splitSidebarWidth });
784 async onSidebarToggle() {
785 this.is3PaneModeEnabled = !this.is3PaneModeEnabled;
786 await this.setupToolbar();
787 await this.addRuleView({ skipQueue: true });
791 * Sets the inspector sidebar split box state. Shows the splitter inside the sidebar
792 * split box, specifies the end panel control and resizes the split box width depending
793 * on the width of the toolbox.
795 setSidebarSplitBoxState() {
796 const toolboxWidth = this.panelDoc.getElementById("inspector-splitter-box")
799 // Get the inspector sidebar's (right panel in horizontal mode or bottom panel in
800 // vertical mode) width.
801 const sidebarWidth = this.splitBox.state.width;
802 // This variable represents the width of the right panel in horizontal mode or
803 // bottom-right panel in vertical mode width in 3 pane mode.
804 let sidebarSplitboxWidth;
806 if (this.useLandscapeMode()) {
807 // Whether or not doubling the inspector sidebar's (right panel in horizontal mode
808 // or bottom panel in vertical mode) width will be bigger than half of the
810 const canDoubleSidebarWidth = sidebarWidth * 2 < toolboxWidth / 2;
812 // Resize the main split box's end panel that contains the middle and right panel.
813 // Attempts to resize the main split box's end panel to be double the size of the
814 // existing sidebar's width when switching to 3 pane mode. However, if the middle
815 // and right panel's width together is greater than half of the toolbox's width,
816 // split all 3 panels to be equally sized by resizing the end panel to be 2/3 of
817 // the current toolbox's width.
818 this.splitBox.setState({
819 width: canDoubleSidebarWidth
821 : (toolboxWidth * 2) / 3,
824 // In landscape/horizontal mode, set the right panel back to its original
825 // inspector sidebar width if we can double the sidebar width. Otherwise, set
826 // the width of the right panel to be 1/3 of the toolbox's width since all 3
827 // panels will be equally sized.
828 sidebarSplitboxWidth = canDoubleSidebarWidth
832 // In portrait/vertical mode, set the bottom-right panel to be 1/2 of the
834 sidebarSplitboxWidth = toolboxWidth / 2;
837 // Show the splitter inside the sidebar split box. Sets the width of the inspector
838 // sidebar and specify that the end (right in horizontal or bottom-right in
839 // vertical) panel of the sidebar split box should be controlled when resizing.
840 this.sidebarSplitBoxRef.current.setState({
841 endPanelControl: true,
843 width: sidebarSplitboxWidth,
848 * Adds the rule view to the middle (in landscape/horizontal mode) or bottom-left panel
849 * (in portrait/vertical mode) or inspector sidebar depending on whether or not it is 3
850 * pane mode. The default tab specifies whether or not the rule view should be selected.
851 * The defaultTab defaults to the rule view when reverting to the 2 pane mode and the
852 * rule view is being merged back into the inspector sidebar from middle/bottom-left
853 * panel. Otherwise, we specify the default tab when handling the sidebar setup.
855 * @params {String} defaultTab
856 * Thie id of the default tab for the sidebar.
858 async addRuleView({ defaultTab = "ruleview", skipQueue = false } = {}) {
859 const ruleViewSidebar = this.sidebarSplitBoxRef.current.startPanelContainer;
861 if (this.is3PaneModeEnabled) {
862 // Convert to 3 pane mode by removing the rule view from the inspector sidebar
863 // and adding the rule view to the middle (in landscape/horizontal mode) or
864 // bottom-left (in portrait/vertical mode) panel.
865 ruleViewSidebar.style.display = "block";
867 this.setSidebarSplitBoxState();
869 // Force the rule view panel creation by calling getPanel
870 this.getPanel("ruleview");
872 await this.sidebar.removeTab("ruleview");
874 this.ruleViewSideBar.addExistingTab(
876 INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
880 this.ruleViewSideBar.show();
882 // Removes the rule view from the 3 pane mode and adds the rule view to the main
883 // inspector sidebar.
884 ruleViewSidebar.style.display = "none";
886 // Set the width of the split box (right panel in horziontal mode and bottom panel
887 // in vertical mode) to be the width of the inspector sidebar.
888 const splitterBox = this.panelDoc.getElementById(
889 "inspector-splitter-box"
891 this.splitBox.setState({
892 width: this.useLandscapeMode()
893 ? this.sidebarSplitBoxRef.current.state.width
894 : splitterBox.clientWidth,
897 // Hide the splitter to prevent any drag events in the sidebar split box and
898 // specify that the end (right panel in horziontal mode or bottom panel in vertical
899 // mode) panel should be uncontrolled when resizing.
900 this.sidebarSplitBoxRef.current.setState({
901 endPanelControl: false,
905 this.ruleViewSideBar.hide();
906 await this.ruleViewSideBar.removeTab("ruleview");
909 this.sidebar.addExistingTab(
911 INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
912 defaultTab == "ruleview",
916 this.sidebar.queueExistingTab(
918 INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
919 defaultTab == "ruleview",
925 this.emit("ruleview-added");
929 * Lazily get and create panel instances displayed in the sidebar
931 getPanel: function(id) {
932 if (this._panels.has(id)) {
933 return this._panels.get(id);
938 case "animationinspector":
939 const AnimationInspector = this.browserRequire(
940 "devtools/client/inspector/animation/animation"
942 panel = new AnimationInspector(this, this.panelWin);
945 // box-model isn't a panel on its own, it used to, now it is being used by
946 // the layout view which retrieves an instance via getPanel.
947 const BoxModel = require("devtools/client/inspector/boxmodel/box-model");
948 panel = new BoxModel(this, this.panelWin);
951 const ChangesView = this.browserRequire(
952 "devtools/client/inspector/changes/ChangesView"
954 panel = new ChangesView(this, this.panelWin);
957 const { ComputedViewTool } = this.browserRequire(
958 "devtools/client/inspector/computed/computed"
960 panel = new ComputedViewTool(this, this.panelWin);
962 case "fontinspector":
963 const FontInspector = this.browserRequire(
964 "devtools/client/inspector/fonts/fonts"
966 panel = new FontInspector(this, this.panelWin);
969 const LayoutView = this.browserRequire(
970 "devtools/client/inspector/layout/layout"
972 panel = new LayoutView(this, this.panelWin);
975 const RulesView = this.browserRequire(
976 "devtools/client/inspector/rules/new-rules"
978 panel = new RulesView(this, this.panelWin);
983 } = require("devtools/client/inspector/rules/rules");
984 panel = new RuleViewTool(this, this.panelWin);
987 // This is a custom panel or a non lazy-loaded one.
992 this._panels.set(id, panel);
1001 async setupSidebar() {
1002 const sidebar = this.panelDoc.getElementById("inspector-sidebar");
1004 showAllTabsMenu: true,
1005 sidebarToggleButton: {
1006 collapsed: !this.is3PaneModeEnabled,
1007 collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.hideThreePaneMode"),
1008 expandPaneTitle: INSPECTOR_L10N.getStr("inspector.showThreePaneMode"),
1009 onClick: this.onSidebarToggle,
1013 this.sidebar = new ToolSidebar(sidebar, this, "inspector", options);
1014 this.sidebar.on("select", this.onSidebarSelect);
1016 const ruleSideBar = this.panelDoc.getElementById("inspector-rules-sidebar");
1017 this.ruleViewSideBar = new ToolSidebar(ruleSideBar, this, "inspector", {
1018 hideTabstripe: true,
1021 // defaultTab may also be an empty string or a tab id that doesn't exist anymore
1022 // (e.g. it was a tab registered by an addon that has been uninstalled).
1023 let defaultTab = Services.prefs.getCharPref(
1024 "devtools.inspector.activeSidebar"
1027 if (this.is3PaneModeEnabled && defaultTab === "ruleview") {
1028 defaultTab = "layoutview";
1031 // Append all side panels
1033 await this.addRuleView({ defaultTab });
1035 // Inspector sidebar panels in order of appearance.
1036 const sidebarPanels = [
1039 title: INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2"),
1043 title: INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
1047 title: INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle"),
1050 id: "fontinspector",
1051 title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
1054 id: "animationinspector",
1055 title: INSPECTOR_L10N.getStr(
1056 "inspector.sidebar.animationInspectorTitle"
1062 Services.prefs.getBoolPref("devtools.inspector.new-rulesview.enabled")
1064 sidebarPanels.push({
1066 title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
1070 for (const { id, title } of sidebarPanels) {
1071 // The Computed panel is not a React-based panel. We pick its element container from
1072 // the DOM and wrap it in a React component (InspectorTabPanel) so it behaves like
1073 // other panels when using the Inspector's tool sidebar.
1074 if (id === "computedview") {
1075 this.sidebar.queueExistingTab(id, title, defaultTab === id);
1077 // When `panel` is a function, it is called when the tab should render. It is
1078 // expected to return a React component to populate the tab's content area.
1079 // Calling this method on-demand allows us to lazy-load the requested panel.
1080 this.sidebar.queueTab(
1089 return this.getPanel(id).provider;
1097 this.sidebar.addAllQueuedTabs();
1099 // Persist splitter state in preferences.
1100 this.sidebar.on("show", this.onSidebarShown);
1101 this.sidebar.on("hide", this.onSidebarHidden);
1102 this.sidebar.on("destroy", this.onSidebarHidden);
1104 this.sidebar.show();
1108 * Setup any extension sidebar already registered to the toolbox when the inspector.
1109 * has been created for the first time.
1111 setupExtensionSidebars() {
1112 for (const [sidebarId, { title }] of this.toolbox
1113 .inspectorExtensionSidebars) {
1114 this.addExtensionSidebar(sidebarId, { title });
1119 * Create a side-panel tab controlled by an extension
1120 * using the devtools.panels.elements.createSidebarPane and sidebar object API
1122 * @param {String} id
1123 * An unique id for the sidebar tab.
1124 * @param {Object} options
1125 * @param {String} options.title
1128 addExtensionSidebar: function(id, { title }) {
1129 if (this._panels.has(id)) {
1131 `Cannot create an extension sidebar for the existent id: ${id}`
1135 const extensionSidebar = new ExtensionSidebar(this, { id, title });
1137 // TODO(rpl): pass some extension metadata (e.g. extension name and icon) to customize
1138 // the render of the extension title (e.g. use the icon in the sidebar and show the
1139 // extension name in a tooltip).
1140 this.addSidebarTab(id, title, extensionSidebar.provider, false);
1142 this._panels.set(id, extensionSidebar);
1144 // Emit the created ExtensionSidebar instance to the listeners registered
1145 // on the toolbox by the "devtools.panels.elements" WebExtensions API.
1146 this.toolbox.emit(`extension-sidebar-created-${id}`, extensionSidebar);
1150 * Remove and destroy a side-panel tab controlled by an extension (e.g. when the
1151 * extension has been disable/uninstalled while the toolbox and inspector were
1154 * @param {String} id
1155 * The id of the sidebar tab to destroy.
1157 removeExtensionSidebar: function(id) {
1158 if (!this._panels.has(id)) {
1159 throw new Error(`Unable to find a sidebar panel with id "${id}"`);
1162 const panel = this._panels.get(id);
1164 if (!(panel instanceof ExtensionSidebar)) {
1166 `The sidebar panel with id "${id}" is not an ExtensionSidebar`
1170 this._panels.delete(id);
1171 this.sidebar.removeTab(id);
1176 * Register a side-panel tab. This API can be used outside of
1177 * DevTools (e.g. from an extension) as well as by DevTools
1180 * @param {string} tab uniq id
1181 * @param {string} title tab title
1182 * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
1183 * @param {boolean} selected true if the panel should be selected
1185 addSidebarTab: function(id, title, panel, selected) {
1186 this.sidebar.addTab(id, title, panel, selected);
1190 * Method to check whether the document is a HTML document and
1191 * pickColorFromPage method is available or not.
1193 * @return {Boolean} true if the eyedropper highlighter is supported by the current
1196 async supportsEyeDropper() {
1198 return await this.inspectorFront.supportsHighlighters();
1205 async setupToolbar() {
1206 this.teardownToolbar();
1208 // Setup the add-node button.
1209 this.addNode = this.addNode.bind(this);
1210 this.addNodeButton = this.panelDoc.getElementById(
1211 "inspector-element-add-button"
1213 this.addNodeButton.addEventListener("click", this.addNode);
1215 // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
1216 const canShowEyeDropper = await this.supportsEyeDropper();
1218 // Bail out if the inspector was destroyed in the meantime and panelDoc is no longer
1220 if (!this.panelDoc) {
1224 if (canShowEyeDropper) {
1225 this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
1226 this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(
1229 this.eyeDropperButton = this.panelDoc.getElementById(
1230 "inspector-eyedropper-toggle"
1232 this.eyeDropperButton.disabled = false;
1233 this.eyeDropperButton.title = INSPECTOR_L10N.getStr(
1234 "inspector.eyedropper.label"
1236 this.eyeDropperButton.addEventListener(
1238 this.onEyeDropperButtonClicked
1241 const eyeDropperButton = this.panelDoc.getElementById(
1242 "inspector-eyedropper-toggle"
1244 eyeDropperButton.disabled = true;
1245 eyeDropperButton.title = INSPECTOR_L10N.getStr(
1246 "eyedropper.disabled.title"
1250 this.emit("inspector-toolbar-updated");
1253 teardownToolbar: function() {
1254 if (this.addNodeButton) {
1255 this.addNodeButton.removeEventListener("click", this.addNode);
1256 this.addNodeButton = null;
1259 if (this.eyeDropperButton) {
1260 this.eyeDropperButton.removeEventListener(
1262 this.onEyeDropperButtonClicked
1264 this.eyeDropperButton = null;
1269 * Reset the inspector on new root mutation.
1271 onNewRoot: function() {
1272 // Record new-root timing for telemetry
1273 this._newRootStart = this.panelWin.performance.now();
1275 this._defaultNode = null;
1276 this.selection.setNodeFront(null);
1277 this._destroyMarkup();
1279 const onNodeSelected = defaultNode => {
1280 // Cancel this promise resolution as a new one had
1282 if (this._pendingSelection != onNodeSelected) {
1285 this._pendingSelection = null;
1286 this.selection.setNodeFront(defaultNode, { reason: "navigateaway" });
1288 this.once("markuploaded", this.onMarkupLoaded);
1291 // Setup the toolbar again, since its content may depend on the current document.
1292 this.setupToolbar();
1294 this._pendingSelection = onNodeSelected;
1295 this._getDefaultNodeForSelection().then(
1297 this._handleRejectionIfNotDestroyed
1302 * When replaying, reset the inspector whenever the target pauses.
1304 handleThreadPaused() {
1305 this._replayResumed = false;
1310 * When replaying, reset the inspector whenever the target resumes.
1312 handleThreadResumed() {
1313 this._replayResumed = true;
1318 * Handler for "markuploaded" event fired on a new root mutation and after the markup
1319 * view is initialized. Expands the current selected node and restores the saved
1320 * highlighter state.
1322 async onMarkupLoaded() {
1327 const onExpand = this.markup.expandNode(this.selection.nodeFront);
1329 // Restore the highlighter states prior to emitting "new-root".
1330 if (this._highlighters) {
1332 this.highlighters.restoreFlexboxState(),
1333 this.highlighters.restoreGridState(),
1337 this.emit("new-root");
1339 // Wait for full expand of the selected node in order to ensure
1340 // the markup view is fully emitted before firing 'reloaded'.
1341 // 'reloaded' is used to know when the panel is fully updated
1342 // after a page reload.
1345 this.emit("reloaded");
1347 // Record the time between new-root event and inspector fully loaded.
1348 if (this._newRootStart) {
1349 // Only log the timing when inspector is not destroyed and is in foreground.
1350 if (this.toolbox && this.toolbox.currentToolId == "inspector") {
1351 const delay = this.panelWin.performance.now() - this._newRootStart;
1352 const telemetryKey = "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS";
1353 const histogram = this.telemetry.getHistogramById(telemetryKey);
1354 histogram.add(delay);
1356 delete this._newRootStart;
1360 _selectionCssSelector: null,
1363 * Set the currently selected node unique css selector.
1364 * Will store the current target url along with it to allow pre-selection at
1367 set selectionCssSelector(cssSelector = null) {
1368 if (this._destroyed) {
1372 this._selectionCssSelector = {
1373 selector: cssSelector,
1374 url: this._target.url,
1379 * Get the current selection unique css selector if any, that is, if a node
1380 * is actually selected and that node has been selected while on the same url
1382 get selectionCssSelector() {
1384 this._selectionCssSelector &&
1385 this._selectionCssSelector.url === this._target.url
1387 return this._selectionCssSelector.selector;
1393 * On any new selection made by the user, store the unique css selector
1394 * of the selected node so it can be restored after reload of the same page
1396 updateSelectionCssSelector() {
1397 if (this.selection.isElementNode()) {
1398 this.selection.nodeFront.getUniqueSelector().then(selector => {
1399 this.selectionCssSelector = selector;
1400 }, this._handleRejectionIfNotDestroyed);
1405 * Can a new HTML element be inserted into the currently selected element?
1408 canAddHTMLChild: function() {
1409 const selection = this.selection;
1411 // Don't allow to insert an element into these elements. This should only
1412 // contain elements where walker.insertAdjacentHTML has no effect.
1413 const invalidTagNames = ["html", "iframe"];
1416 selection.isHTMLNode() &&
1417 selection.isElementNode() &&
1418 !selection.isPseudoElementNode() &&
1419 !selection.isAnonymousNode() &&
1420 !invalidTagNames.includes(selection.nodeFront.nodeName.toLowerCase())
1425 * Update the state of the add button in the toolbar depending on the current selection.
1427 updateAddElementButton() {
1428 const btn = this.panelDoc.getElementById("inspector-element-add-button");
1429 if (this.canAddHTMLChild()) {
1430 btn.removeAttribute("disabled");
1432 btn.setAttribute("disabled", "true");
1437 * Handler for the "host-changed" event from the toolbox. Resets the inspector
1438 * sidebar sizes when the toolbox host type changes.
1440 async onHostChanged() {
1441 // Eagerly call our resize handling code to process the fact that we
1442 // switched hosts. If we don't do this, we'll wait for resize events + 200ms
1443 // to have passed, which causes the old layout to noticeably show up in the
1444 // new host, followed by the updated one.
1445 await this._onLazyPanelResize();
1446 // Note that we may have been destroyed by now, especially in tests, so we
1447 // need to check if that's happened before touching anything else.
1448 if (!this.target || !this.is3PaneModeEnabled) {
1452 // When changing hosts, the toolbox chromeEventHandler might change, for instance when
1453 // switching from docked to window hosts. Recreate the searchbox shortcuts.
1454 this.searchboxShortcuts.destroy();
1455 this.createSearchBoxShortcuts();
1457 this.setSidebarSplitBoxState();
1461 * When a new node is selected.
1463 onNewSelection: function(value, reason) {
1464 if (reason === "selection-destroy") {
1468 this.updateAddElementButton();
1469 this.updateSelectionCssSelector();
1471 const selfUpdate = this.updating("inspector-panel");
1474 selfUpdate(this.selection.nodeFront);
1475 this.telemetry.scalarAdd(TELEMETRY_SCALAR_NODE_SELECTION_COUNT, 1);
1483 * Delay the "inspector-updated" notification while a tool
1484 * is updating itself. Returns a function that must be
1485 * invoked when the tool is done updating with the node
1486 * that the tool is viewing.
1488 updating: function(name) {
1490 this._updateProgress &&
1491 this._updateProgress.node != this.selection.nodeFront
1493 this.cancelUpdate();
1496 if (!this._updateProgress) {
1497 // Start an update in progress.
1499 this._updateProgress = {
1500 node: this.selection.nodeFront,
1501 outstanding: new Set(),
1502 checkDone: function() {
1503 if (this !== self._updateProgress) {
1506 // Cancel update if there is no `selection` anymore.
1507 // It can happen if the inspector panel is already destroyed.
1508 if (!self.selection || this.node !== self.selection.nodeFront) {
1509 self.cancelUpdate();
1512 if (this.outstanding.size !== 0) {
1516 self._updateProgress = null;
1517 self.emit("inspector-updated", name);
1522 const progress = this._updateProgress;
1523 const done = function() {
1524 progress.outstanding.delete(done);
1525 progress.checkDone();
1527 progress.outstanding.add(done);
1532 * Cancel notification of inspector updates.
1534 cancelUpdate: function() {
1535 this._updateProgress = null;
1539 * When a node is deleted, select its parent node or the defaultNode if no
1540 * parent is found (may happen when deleting an iframe inside which the
1541 * node was selected).
1543 onDetached: function(parentNode) {
1544 this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
1545 const nodeFront = parentNode ? parentNode : this._defaultNode;
1546 this.selection.setNodeFront(nodeFront, { reason: "detached" });
1550 * Destroy the inspector.
1552 destroy: function() {
1553 if (this._destroyed) {
1556 this._destroyed = true;
1558 this._target.threadFront.off("paused", this.handleThreadPaused);
1559 this._target.threadFront.off("resumed", this.handleThreadResumed);
1562 this.walker.off("new-root", this.onNewRoot);
1563 this.pageStyle = null;
1566 this.cancelUpdate();
1568 this.sidebar.destroy();
1570 this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
1571 this.selection.off("new-node-front", this.onNewSelection);
1572 this.selection.off("detached-front", this.onDetached);
1573 this.sidebar.off("select", this.onSidebarSelect);
1574 this.sidebar.off("show", this.onSidebarShown);
1575 this.sidebar.off("hide", this.onSidebarHidden);
1576 this.sidebar.off("destroy", this.onSidebarHidden);
1577 this.target.off("will-navigate", this._onBeforeNavigate);
1579 for (const [, panel] of this._panels) {
1582 this._panels.clear();
1584 if (this._highlighters) {
1585 this._highlighters.destroy();
1586 this._highlighters = null;
1589 if (this._markupFrame) {
1590 this._markupFrame.removeEventListener(
1592 this._onMarkupFrameLoad,
1598 this._search.destroy();
1599 this._search = null;
1602 this.sidebar.destroy();
1603 if (this.ruleViewSideBar) {
1604 this.ruleViewSideBar.destroy();
1606 this._destroyMarkup();
1608 this.teardownToolbar();
1610 this.breadcrumbs.destroy();
1611 this.reflowTracker.destroy();
1612 this.styleChangeTracker.destroy();
1613 this.searchboxShortcuts.destroy();
1615 this._is3PaneModeChromeEnabled = null;
1616 this._is3PaneModeEnabled = null;
1617 this._markupBox = null;
1618 this._markupFrame = null;
1619 this._target = null;
1620 this._toolbox = null;
1621 this.breadcrumbs = null;
1622 this.panelDoc = null;
1623 this.panelWin.inspector = null;
1624 this.panelWin = null;
1625 this.resultsLength = null;
1626 this.searchBox = null;
1627 this.show3PaneTooltip = null;
1628 this.sidebar = null;
1630 this.telemetry = null;
1633 _initMarkup: function() {
1634 if (!this._markupFrame) {
1635 this._markupFrame = this.panelDoc.createElement("iframe");
1636 this._markupFrame.setAttribute(
1638 INSPECTOR_L10N.getStr("inspector.panelLabel.markupView")
1640 this._markupFrame.setAttribute("flex", "1");
1641 // This is needed to enable tooltips inside the iframe document.
1642 this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
1644 this._markupBox.style.visibility = "hidden";
1645 this._markupBox.appendChild(this._markupFrame);
1647 this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
1648 this._markupFrame.setAttribute("src", "markup/markup.xhtml");
1650 this._onMarkupFrameLoad();
1654 _onMarkupFrameLoad: function() {
1655 this._markupFrame.removeEventListener(
1657 this._onMarkupFrameLoad,
1660 this._markupFrame.contentWindow.focus();
1661 this._markupBox.style.visibility = "visible";
1662 this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
1663 this.emit("markuploaded");
1666 _destroyMarkup: function() {
1670 destroyPromise = this.markup.destroy();
1673 destroyPromise = promise.resolve();
1676 this._markupBox.style.visibility = "hidden";
1678 return destroyPromise;
1681 onEyeDropperButtonClicked: function() {
1682 this.eyeDropperButton.classList.contains("checked")
1683 ? this.hideEyeDropper()
1684 : this.showEyeDropper();
1687 startEyeDropperListeners: function() {
1688 this.inspectorFront.once("color-pick-canceled", this.onEyeDropperDone);
1689 this.inspectorFront.once("color-picked", this.onEyeDropperDone);
1690 this.walker.once("new-root", this.onEyeDropperDone);
1693 stopEyeDropperListeners: function() {
1694 this.inspectorFront.off("color-pick-canceled", this.onEyeDropperDone);
1695 this.inspectorFront.off("color-picked", this.onEyeDropperDone);
1696 this.walker.off("new-root", this.onEyeDropperDone);
1699 onEyeDropperDone: function() {
1700 this.eyeDropperButton.classList.remove("checked");
1701 this.stopEyeDropperListeners();
1705 * Show the eyedropper on the page.
1706 * @return {Promise} resolves when the eyedropper is visible.
1708 showEyeDropper: function() {
1709 // The eyedropper button doesn't exist, most probably because the actor doesn't
1710 // support the pickColorFromPage, or because the page isn't HTML.
1711 if (!this.eyeDropperButton) {
1715 this.telemetry.scalarSet(TELEMETRY_EYEDROPPER_OPENED, 1);
1716 this.eyeDropperButton.classList.add("checked");
1717 this.startEyeDropperListeners();
1718 return this.inspectorFront
1719 .pickColorFromPage({ copyOnSelect: true })
1720 .catch(console.error);
1724 * Hide the eyedropper.
1725 * @return {Promise} resolves when the eyedropper is hidden.
1727 hideEyeDropper: function() {
1728 // The eyedropper button doesn't exist, most probably because the page isn't HTML.
1729 if (!this.eyeDropperButton) {
1733 this.eyeDropperButton.classList.remove("checked");
1734 this.stopEyeDropperListeners();
1735 return this.inspectorFront.cancelPickColorFromPage().catch(console.error);
1739 * Create a new node as the last child of the current selection, expand the
1740 * parent and select the new node.
1743 if (!this.canAddHTMLChild()) {
1747 const html = "<div></div>";
1749 // Insert the html and expect a childList markup mutation.
1750 const onMutations = this.once("markupmutation");
1751 await this.walker.insertAdjacentHTML(
1752 this.selection.nodeFront,
1758 // Expand the parent node.
1759 this.markup.expandNode(this.selection.nodeFront);
1763 * Toggle a pseudo class.
1765 togglePseudoClass: function(pseudo) {
1766 if (this.selection.isElementNode()) {
1767 const node = this.selection.nodeFront;
1768 if (node.hasPseudoClassLock(pseudo)) {
1769 return this.walker.removePseudoClassLock(node, pseudo, {
1774 const hierarchical = pseudo == ":hover" || pseudo == ":active";
1775 return this.walker.addPseudoClassLock(node, pseudo, {
1776 parents: hierarchical,
1779 return promise.resolve();
1783 * Initiate screenshot command on selected node.
1785 async screenshotNode() {
1786 // Bug 1332936 - it's possible to call `screenshotNode` while the BoxModel highlighter
1787 // is still visible, therefore showing it in the picture.
1788 // To avoid that, we have to hide it before taking the screenshot. The `hideBoxModel`
1789 // will do that, calling `hide` for the highlighter only if previously shown.
1790 await this.highlighter.hideBoxModel();
1792 const clipboardEnabled = Services.prefs.getBoolPref(
1793 "devtools.screenshot.clipboard.enabled"
1797 nodeActorID: this.selection.nodeFront.actorID,
1798 clipboard: clipboardEnabled,
1800 const screenshotFront = await this.target.getFront("screenshot");
1801 const screenshot = await screenshotFront.capture(args);
1802 await saveScreenshot(this.panelWin, args, screenshot);
1806 * Returns an object containing the shared handler functions used in the box
1807 * model and grid React components.
1809 getCommonComponentProps() {
1811 setSelectedNode: this.selection.setNodeFront,
1812 onShowBoxModelHighlighterForNode: this.onShowBoxModelHighlighterForNode,
1817 * Shows the box-model highlighter on the element corresponding to the provided
1820 * @param {NodeFront} nodeFront
1821 * The node to highlight.
1822 * @param {Object} options
1823 * Options passed to the highlighter actor.
1825 onShowBoxModelHighlighterForNode(nodeFront, options) {
1826 const toolbox = this.toolbox;
1827 toolbox.highlighter.highlight(nodeFront, options);
1830 async inspectNodeActor(nodeActor, inspectFromAnnotation) {
1831 const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
1834 "The object cannot be linked to the inspector, the " +
1835 "corresponding nodeFront could not be found."
1840 const isAttached = await this.walker.isInDOMTree(nodeFront);
1842 console.error("Selected DOMNode is not attached to the document tree.");
1846 await this.selection.setNodeFront(nodeFront, {
1847 reason: inspectFromAnnotation,
1853 exports.Inspector = Inspector;