panel.js (7169B)
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 "use strict"; 5 6 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 7 loader.lazyRequireGetter( 8 this, 9 "openContentLink", 10 "resource://devtools/client/shared/link.js", 11 true 12 ); 13 14 /** 15 * This object represents DOM panel. It's responsibility is to 16 * render Document Object Model of the current debugger target. 17 */ 18 class DomPanel { 19 constructor(iframeWindow, toolbox, commands) { 20 this.panelWin = iframeWindow; 21 this._toolbox = toolbox; 22 this._commands = commands; 23 24 this.onContentMessage = this.onContentMessage.bind(this); 25 this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this); 26 27 this.pendingRequests = new Map(); 28 29 EventEmitter.decorate(this); 30 } 31 /** 32 * Open is effectively an asynchronous constructor. 33 * 34 * @return object 35 * A promise that is resolved when the DOM panel completes opening. 36 */ 37 async open() { 38 // Wait for the retrieval of root object properties before resolving open 39 const onGetProperties = new Promise(resolve => { 40 this._resolveOpen = resolve; 41 }); 42 43 await this.initialize(); 44 45 await onGetProperties; 46 47 return this; 48 } 49 50 // Initialization 51 async initialize() { 52 this.panelWin.addEventListener( 53 "devtools/content/message", 54 this.onContentMessage, 55 true 56 ); 57 58 this._toolbox.on("select", this.onPanelVisibilityChange); 59 60 // onTargetAvailable is mandatory when calling watchTargets 61 this._onTargetAvailable = () => {}; 62 this._onTargetSelected = this._onTargetSelected.bind(this); 63 await this._commands.targetCommand.watchTargets({ 64 types: [this._commands.targetCommand.TYPES.FRAME], 65 onAvailable: this._onTargetAvailable, 66 onSelected: this._onTargetSelected, 67 }); 68 69 this.onResourceAvailable = this.onResourceAvailable.bind(this); 70 await this._commands.resourceCommand.watchResources( 71 [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT], 72 { 73 onAvailable: this.onResourceAvailable, 74 } 75 ); 76 77 // Export provider object with useful API for DOM panel. 78 const provider = { 79 getToolbox: this.getToolbox.bind(this), 80 getPrototypeAndProperties: this.getPrototypeAndProperties.bind(this), 81 openLink: this.openLink.bind(this), 82 // Resolve DomPanel.open once the object properties are fetched 83 onPropertiesFetched: () => { 84 if (this._resolveOpen) { 85 this._resolveOpen(); 86 this._resolveOpen = null; 87 } 88 }, 89 }; 90 91 exportIntoContentScope(this.panelWin, provider, "DomProvider"); 92 } 93 94 destroy() { 95 if (this._destroyed) { 96 return; 97 } 98 this._destroyed = true; 99 100 this._commands.targetCommand.unwatchTargets({ 101 types: [this._commands.targetCommand.TYPES.FRAME], 102 onAvailable: this._onTargetAvailable, 103 onSelected: this._onTargetSelected, 104 }); 105 this._commands.resourceCommand.unwatchResources( 106 [this._commands.resourceCommand.TYPES.DOCUMENT_EVENT], 107 { onAvailable: this.onResourceAvailable } 108 ); 109 this._toolbox.off("select", this.onPanelVisibilityChange); 110 111 this.emit("destroyed"); 112 } 113 114 // Events 115 refresh() { 116 // Do not refresh if the panel isn't visible. 117 if (!this.isPanelVisible()) { 118 return; 119 } 120 121 // Do not refresh if it isn't necessary. 122 if (!this.shouldRefresh) { 123 return; 124 } 125 126 // Alright reset the flag we are about to refresh the panel. 127 this.shouldRefresh = false; 128 129 this.getRootGrip().then(rootGrip => { 130 this.postContentMessage("initialize", rootGrip); 131 }); 132 } 133 134 /** 135 * Make sure the panel is refreshed, either when navigation occurs or when a frame is 136 * selected in the iframe picker. 137 * The panel is refreshed immediately if it's currently selected or lazily when the user 138 * actually selects it. 139 */ 140 forceRefresh() { 141 this.shouldRefresh = true; 142 // This will end up calling scriptCommand execute method to retrieve the `window` grip 143 // on targetCommand.selectedTargetFront. 144 this.refresh(); 145 } 146 147 _onTargetSelected() { 148 this.forceRefresh(); 149 } 150 151 onResourceAvailable(resources) { 152 for (const resource of resources) { 153 // Only consider top level document, and ignore remote iframes top document 154 if ( 155 resource.resourceType === 156 this._commands.resourceCommand.TYPES.DOCUMENT_EVENT && 157 resource.name === "dom-complete" && 158 resource.targetFront.isTopLevel 159 ) { 160 this.forceRefresh(); 161 } 162 } 163 } 164 165 /** 166 * Make sure the panel is refreshed (if needed) when it's selected. 167 */ 168 onPanelVisibilityChange() { 169 this.refresh(); 170 } 171 172 // Helpers 173 /** 174 * Return true if the DOM panel is currently selected. 175 */ 176 isPanelVisible() { 177 return this._toolbox.currentToolId === "dom"; 178 } 179 180 async getPrototypeAndProperties(objectFront) { 181 if (!objectFront.actorID) { 182 console.error("No actor!", objectFront); 183 throw new Error("Failed to get object front."); 184 } 185 186 // Bail out if target doesn't exist (toolbox maybe closed already). 187 if (!this.currentTarget) { 188 return null; 189 } 190 191 // Check for a previously stored request for grip. 192 let request = this.pendingRequests.get(objectFront.actorID); 193 194 // If no request is in progress create a new one. 195 if (!request) { 196 request = objectFront.getPrototypeAndProperties(); 197 this.pendingRequests.set(objectFront.actorID, request); 198 } 199 200 const response = await request; 201 this.pendingRequests.delete(objectFront.actorID); 202 203 // Fire an event about not having any pending requests. 204 if (!this.pendingRequests.size) { 205 this.emit("no-pending-requests"); 206 } 207 208 return response; 209 } 210 211 openLink(url) { 212 openContentLink(url); 213 } 214 215 async getRootGrip() { 216 const { result } = await this._toolbox.commands.scriptCommand.execute( 217 "window", 218 { 219 disableBreaks: true, 220 } 221 ); 222 return result; 223 } 224 225 postContentMessage(type, args) { 226 const data = { 227 type, 228 args, 229 }; 230 231 const event = new this.panelWin.MessageEvent("devtools/chrome/message", { 232 bubbles: true, 233 cancelable: true, 234 data, 235 }); 236 237 this.panelWin.dispatchEvent(event); 238 } 239 240 onContentMessage(event) { 241 const data = event.data; 242 const method = data.type; 243 if (typeof this[method] == "function") { 244 this[method](data.args); 245 } 246 } 247 248 getToolbox() { 249 return this._toolbox; 250 } 251 252 get currentTarget() { 253 return this._toolbox.target; 254 } 255 } 256 257 // Helpers 258 259 function exportIntoContentScope(win, obj, defineAs) { 260 const clone = Cu.createObjectIn(win, { 261 defineAs, 262 }); 263 264 const props = Object.getOwnPropertyNames(obj); 265 for (let i = 0; i < props.length; i++) { 266 const propName = props[i]; 267 const propValue = obj[propName]; 268 if (typeof propValue == "function") { 269 Cu.exportFunction(propValue, clone, { 270 defineAs: propName, 271 }); 272 } 273 } 274 } 275 276 // Exports from this module 277 exports.DomPanel = DomPanel;