Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
loader.lazyRequireGetter(
this,
"openContentLink",
"resource://devtools/client/shared/link.js",
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);
// onTargetAvailable is mandatory when calling watchTargets
this._onTargetAvailable = () => {};
this._onTargetSelected = this._onTargetSelected.bind(this);
await this._commands.targetCommand.watchTargets({
types: [this._commands.targetCommand.TYPES.FRAME],
onAvailable: this._onTargetAvailable,
onSelected: this._onTargetSelected,
});
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.targetCommand.unwatchTargets({
types: [this._commands.targetCommand.TYPES.FRAME],
onAvailable: this._onTargetAvailable,
onSelected: this._onTargetSelected,
});
this._commands.resourceCommand.unwatchResources(
[this._commands.resourceCommand.TYPES.DOCUMENT_EVENT],
{ onAvailable: this.onResourceAvailable }
);
this._toolbox.off("select", this.onPanelVisibilityChange);
this.emit("destroyed");
},
// Events
refresh() {
// 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, either when navigation occurs or when a frame is
* selected in the iframe picker.
* The panel is refreshed immediately if it's currently selected or lazily when the user
* actually selects it.
*/
forceRefresh() {
this.shouldRefresh = true;
// This will end up calling scriptCommand execute method to retrieve the `window` grip
// on targetCommand.selectedTargetFront.
this.refresh();
},
_onTargetSelected() {
this.forceRefresh();
},
onResourceAvailable(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.forceRefresh();
}
}
},
/**
* Make sure the panel is refreshed (if needed) when it's selected.
*/
onPanelVisibilityChange() {
this.refresh();
},
// Helpers
/**
* Return true if the DOM panel is currently selected.
*/
isPanelVisible() {
return this._toolbox.currentToolId === "dom";
},
async getPrototypeAndProperties(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(url) {
openContentLink(url);
},
async getRootGrip() {
const { result } = await this._toolbox.commands.scriptCommand.execute(
"window",
{
disableBreaks: true,
}
);
return result;
},
postContentMessage(type, args) {
const data = {
type,
args,
};
const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
bubbles: true,
cancelable: true,
data,
});
this.panelWin.dispatchEvent(event);
},
onContentMessage(event) {
const data = event.data;
const method = data.type;
if (typeof this[method] == "function") {
this[method](data.args);
}
},
getToolbox() {
return this._toolbox;
},
get currentTarget() {
return this._toolbox.target;
},
};
// Helpers
function exportIntoContentScope(win, obj, defineAs) {
const clone = Cu.createObjectIn(win, {
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;