diff options
Diffstat (limited to 'devtools/client/dom/panel.js')
-rw-r--r-- | devtools/client/dom/panel.js | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/devtools/client/dom/panel.js b/devtools/client/dom/panel.js new file mode 100644 index 0000000000..b7b7ef81ec --- /dev/null +++ b/devtools/client/dom/panel.js @@ -0,0 +1,278 @@ +/* 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({ targetFront }) { + 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" + ); + 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; |