fune/devtools/client/dom/panel.js
Alexandre Poirot b10f35c671 Bug 1715911 - [devtools] Use DOCUMENT_EVENT's dom-complete event in the DOM panel r=nchevobbe
This slightly changes panel initialization,
instead of updating the UI on opening as soon as the first top level target is available,
it will only update once the navigate resource fires.

So if we open the toolbox on a still loading document, it will update later, but only once
instead of twice.

Differential Revision: https://phabricator.services.mozilla.com/D117515
2021-06-15 08:54:40 +00:00

259 lines
6.3 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu } = require("chrome");
const EventEmitter = require("devtools/shared/event-emitter");
loader.lazyRequireGetter(
this,
"openContentLink",
"devtools/client/shared/link",
true
);
/**
* This object represents DOM panel. It's responsibility is to
* render Document Object Model of the current debugger target.
*/
function DomPanel(iframeWindow, toolbox, commands) {
this.panelWin = iframeWindow;
this._toolbox = toolbox;
this._commands = commands;
this.onContentMessage = this.onContentMessage.bind(this);
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
this.pendingRequests = new Map();
EventEmitter.decorate(this);
}
DomPanel.prototype = {
/**
* Open is effectively an asynchronous constructor.
*
* @return object
* A promise that is resolved when the DOM panel completes opening.
*/
async open() {
// Wait for the retrieval of root object properties before resolving open
const onGetProperties = new Promise(resolve => {
this._resolveOpen = resolve;
});
await this.initialize();
await onGetProperties;
return this;
},
// Initialization
async initialize() {
this.panelWin.addEventListener(
"devtools/content/message",
this.onContentMessage,
true
);
this._toolbox.on("select", this.onPanelVisibilityChange);
this.onResourceAvailable = this.onResourceAvailable.bind(this);
await this._commands.resourceCommand.watchResources(
[this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
{
onAvailable: this.onResourceAvailable,
}
);
// Export provider object with useful API for DOM panel.
const provider = {
getToolbox: this.getToolbox.bind(this),
getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this),
openLink: this.openLink.bind(this),
// Resolve DomPanel.open once the object properties are fetched
onPropertiesFetched: () => {
if (this._resolveOpen) {
this._resolveOpen();
this._resolveOpen = null;
}
},
};
exportIntoContentScope(this.panelWin, provider, "DomProvider");
},
destroy() {
if (this._destroyed) {
return;
}
this._destroyed = true;
this._commands.resourceCommand.unwatchResources(
[this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
{ onAvailable: this.onResourceAvailable }
);
this._toolbox.off("select", this.onPanelVisibilityChange);
this.emit("destroyed");
},
// Events
refresh: function() {
// Do not refresh if the panel isn't visible.
if (!this.isPanelVisible()) {
return;
}
// Do not refresh if it isn't necessary.
if (!this.shouldRefresh) {
return;
}
// Alright reset the flag we are about to refresh the panel.
this.shouldRefresh = false;
this.getRootGrip().then(rootGrip => {
this.postContentMessage("initialize", rootGrip);
});
},
/**
* Make sure the panel is refreshed when navigation occurs.
* The panel is refreshed immediately if it's currently selected or lazily when the user
* actually selects it.
*/
onTabNavigated: function() {
this.shouldRefresh = true;
this.refresh();
},
onResourceAvailable: function(resources) {
for (const resource of resources) {
// Only consider top level document, and ignore remote iframes top document
if (
resource.resourceType ===
this._commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
resource.name === "dom-complete" &&
resource.targetFront.isTopLevel
) {
this.onTabNavigated();
}
}
},
/**
* Make sure the panel is refreshed (if needed) when it's selected.
*/
onPanelVisibilityChange: function() {
this.refresh();
},
// Helpers
/**
* Return true if the DOM panel is currently selected.
*/
isPanelVisible: function() {
return this._toolbox.currentToolId === "dom";
},
getPrototypeAndProperties: async function(objectFront) {
if (!objectFront.actorID) {
console.error("No actor!", objectFront);
throw new Error("Failed to get object front.");
}
// Bail out if target doesn't exist (toolbox maybe closed already).
if (!this.currentTarget) {
return null;
}
// Check for a previously stored request for grip.
let request = this.pendingRequests.get(objectFront.actorID);
// If no request is in progress create a new one.
if (!request) {
request = objectFront.getPrototypeAndProperties();
this.pendingRequests.set(objectFront.actorID, request);
}
const response = await request;
this.pendingRequests.delete(objectFront.actorID);
// Fire an event about not having any pending requests.
if (!this.pendingRequests.size) {
this.emit("no-pending-requests");
}
return response;
},
openLink: function(url) {
openContentLink(url);
},
getRootGrip: async function() {
const { result } = await this._toolbox.commands.scriptCommand.execute(
"window"
);
return result;
},
postContentMessage: function(type, args) {
const data = {
type: type,
args: args,
};
const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
bubbles: true,
cancelable: true,
data: data,
});
this.panelWin.dispatchEvent(event);
},
onContentMessage: function(event) {
const data = event.data;
const method = data.type;
if (typeof this[method] == "function") {
this[method](data.args);
}
},
getToolbox: function() {
return this._toolbox;
},
get currentTarget() {
return this._toolbox.target;
},
};
// Helpers
function exportIntoContentScope(win, obj, defineAs) {
const clone = Cu.createObjectIn(win, {
defineAs: defineAs,
});
const props = Object.getOwnPropertyNames(obj);
for (let i = 0; i < props.length; i++) {
const propName = props[i];
const propValue = obj[propName];
if (typeof propValue == "function") {
Cu.exportFunction(propValue, clone, {
defineAs: propName,
});
}
}
}
// Exports from this module
exports.DomPanel = DomPanel;