/* 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;