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