EvaluationContextSelector.js (11627B)
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 5 "use strict"; 6 7 // React & Redux 8 const { 9 Component, 10 createFactory, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 15 const { 16 connect, 17 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 18 19 const targetActions = require("resource://devtools/shared/commands/target/actions/targets.js"); 20 const webconsoleActions = require("resource://devtools/client/webconsole/actions/index.js"); 21 22 const { 23 l10n, 24 } = require("resource://devtools/client/webconsole/utils/messages.js"); 25 const targetSelectors = require("resource://devtools/shared/commands/target/selectors/targets.js"); 26 27 loader.lazyGetter(this, "TARGET_TYPES", function () { 28 return require("resource://devtools/shared/commands/target/target-command.js") 29 .TYPES; 30 }); 31 32 // Additional Components 33 const MenuButton = createFactory( 34 require("resource://devtools/client/shared/components/menu/MenuButton.js") 35 ); 36 37 loader.lazyGetter(this, "MenuItem", function () { 38 return createFactory( 39 require("resource://devtools/client/shared/components/menu/MenuItem.js") 40 ); 41 }); 42 43 loader.lazyGetter(this, "MenuList", function () { 44 return createFactory( 45 require("resource://devtools/client/shared/components/menu/MenuList.js") 46 ); 47 }); 48 49 class EvaluationContextSelector extends Component { 50 static get propTypes() { 51 return { 52 selectTarget: PropTypes.func.isRequired, 53 onContextChange: PropTypes.func.isRequired, 54 selectedTarget: PropTypes.object, 55 lastTargetRefresh: PropTypes.number, 56 targets: PropTypes.array, 57 webConsoleUI: PropTypes.object.isRequired, 58 }; 59 } 60 61 shouldComponentUpdate(nextProps) { 62 if (this.props.selectedTarget !== nextProps.selectedTarget) { 63 return true; 64 } 65 66 if (this.props.lastTargetRefresh !== nextProps.lastTargetRefresh) { 67 return true; 68 } 69 70 if (this.props.targets.length !== nextProps.targets.length) { 71 return true; 72 } 73 74 for (let i = 0; i < nextProps.targets.length; i++) { 75 const target = this.props.targets[i]; 76 const nextTarget = nextProps.targets[i]; 77 if (target.url != nextTarget.url || target.name != nextTarget.name) { 78 return true; 79 } 80 } 81 return false; 82 } 83 84 componentDidUpdate(prevProps) { 85 if (this.props.selectedTarget !== prevProps.selectedTarget) { 86 this.props.onContextChange(); 87 } 88 } 89 90 getIcon(target) { 91 if (target.targetType === TARGET_TYPES.FRAME) { 92 return "chrome://devtools/content/debugger/images/globe-small.svg"; 93 } 94 95 if ( 96 target.targetType === TARGET_TYPES.WORKER || 97 target.targetType === TARGET_TYPES.SHARED_WORKER || 98 target.targetType === TARGET_TYPES.SERVICE_WORKER 99 ) { 100 return "chrome://devtools/content/debugger/images/worker.svg"; 101 } 102 103 if (target.targetType === TARGET_TYPES.PROCESS) { 104 return "chrome://devtools/content/debugger/images/window.svg"; 105 } 106 107 if (target.targetType === TARGET_TYPES.CONTENT_SCRIPT) { 108 return "chrome://devtools/content/debugger/images/sources/extension.svg"; 109 } 110 111 return null; 112 } 113 114 renderMenuItem(target, indented = false) { 115 const { selectTarget, selectedTarget } = this.props; 116 117 // When debugging a Web Extension, the top level target is always the fallback document. 118 // It isn't really a top level document as it won't be the parent of any other. 119 // So only print its name. 120 const label = 121 target.isTopLevel && !target.commands.descriptorFront.isWebExtension 122 ? l10n.getStr("webconsole.input.selector.top") 123 : target.name; 124 125 return MenuItem({ 126 key: `webconsole-evaluation-selector-item-${target.actorID}`, 127 className: `menu-item webconsole-evaluation-selector-item ${ 128 indented ? "indented" : "" 129 }`, 130 type: "checkbox", 131 checked: selectedTarget ? selectedTarget == target : target.isTopLevel, 132 label, 133 tooltip: target.url || target.name, 134 icon: this.getIcon(target), 135 onClick: () => selectTarget(target.actorID), 136 }); 137 } 138 139 renderMenuItems() { 140 const { targets } = this.props; 141 142 // Let's sort the targets (using "numeric" so Content processes are ordered by PID). 143 const collator = new Intl.Collator("en", { numeric: true }); 144 targets.sort((a, b) => collator.compare(a.name, b.name)); 145 146 // When in Browser Toolbox, we want to display the process targets with the frames 147 // in the same process as a group 148 // e.g. 149 // |------------------------------| 150 // | Top | 151 // | -----------------------------| 152 // | (pid 1234) priviledgedabout | 153 // | New Tab | 154 // | -----------------------------| 155 // | (pid 5678) web | 156 // | cnn.com | 157 // | -----------------------------| 158 // | RemoteSettingWorker.js | 159 // |------------------------------| 160 // 161 162 const { webConsoleUI } = this.props; 163 const handleProcessTargets = 164 webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole; 165 166 const processTargets = []; 167 const frameTargets = new Set(); 168 const contentScriptTargets = new Set(); 169 const workerTargets = new Set(); 170 let topTarget = null; 171 172 for (const target of targets) { 173 if (target.isTopLevel) { 174 topTarget = target; 175 continue; 176 } 177 switch (target.targetType) { 178 case TARGET_TYPES.PROCESS: 179 processTargets.push(target); 180 break; 181 case TARGET_TYPES.FRAME: 182 frameTargets.add(target); 183 break; 184 case TARGET_TYPES.CONTENT_SCRIPT: 185 contentScriptTargets.add(target); 186 break; 187 case TARGET_TYPES.WORKER: 188 case TARGET_TYPES.SHARED_WORKER: 189 case TARGET_TYPES.SERVICE_WORKER: 190 workerTargets.add(target); 191 break; 192 default: 193 console.warn( 194 "Unsupported target type in the evalutiong context selector", 195 target.targetType 196 ); 197 } 198 } 199 200 const items = []; 201 202 const renderFrameWithContentScripts = frameTarget => { 203 items.push(this.renderMenuItem(frameTarget)); 204 205 // Render under each frame, its related web extension content scripts,... 206 for (const contentScriptTarget of contentScriptTargets) { 207 if (contentScriptTarget.innerWindowId != frameTarget.innerWindowId) { 208 continue; 209 } 210 items.push(this.renderMenuItem(contentScriptTarget, true)); 211 contentScriptTargets.delete(contentScriptTarget); 212 } 213 214 // ...as well as all its related workers 215 for (const workerTarget of workerTargets) { 216 if ( 217 workerTarget.relatedDocumentInnerWindowId != frameTarget.innerWindowId 218 ) { 219 continue; 220 } 221 items.push(this.renderMenuItem(workerTarget, true)); 222 workerTargets.delete(workerTarget); 223 } 224 }; 225 226 // Note that while debugging popups, we might have a small period 227 // of time where we don't have any top level target when we reload 228 // the original tab 229 if (topTarget) { 230 renderFrameWithContentScripts(topTarget); 231 } 232 233 if (handleProcessTargets) { 234 const sortedProcessTargets = processTargets.sort( 235 (a, b) => a.processID < b.processID 236 ); 237 for (const target of sortedProcessTargets) { 238 items.push( 239 dom.hr({ 240 role: "menuseparator", 241 key: `process-separator-${target.actorID}`, 242 }), 243 this.renderMenuItem(target) 244 ); 245 246 for (const frameTarget of frameTargets) { 247 if (frameTarget.processID != target.processID) { 248 continue; 249 } 250 renderFrameWithContentScripts(frameTarget); 251 frameTargets.delete(frameTarget); 252 } 253 } 254 } 255 256 // Render all targets when running in regular non-browser-console/toolbox, 257 // but also possibly render any leftover frame which can't be matched to any Process ID. 258 const sortedFrames = [...frameTargets].sort( 259 (a, b) => a.innerWindowID < b.innerWindowID 260 ); 261 if (sortedFrames.length) { 262 items.push(dom.hr({ role: "menuseparator", key: `frame-separator` })); 263 } 264 for (const frameTarget of sortedFrames) { 265 renderFrameWithContentScripts(frameTarget); 266 } 267 268 // All content scripts and workers should have matched their related frame target in `renderFrameWithContentScripts`, 269 // but just in case, display any leftover. 270 for (const contentScriptTarget of contentScriptTargets) { 271 items.push(this.renderMenuItem(contentScriptTarget)); 272 } 273 const sortedWorkers = [...workerTargets].sort((a, b) => a.url < b.url); 274 if (sortedWorkers.length) { 275 items.push(dom.hr({ role: "menuseparator", key: `worker-separator` })); 276 } 277 for (const workerTarget of sortedWorkers) { 278 items.push(this.renderMenuItem(workerTarget)); 279 } 280 281 return MenuList( 282 { id: "webconsole-console-evaluation-context-selector-menu-list" }, 283 items 284 ); 285 } 286 287 getLabel() { 288 const { selectedTarget } = this.props; 289 290 // When debugging a Web Extension, the top level target is always the fallback document. 291 // It isn't really a top level document as it won't be the parent of any other. 292 // So only print its name. 293 if ( 294 !selectedTarget || 295 (selectedTarget.isTopLevel && 296 !selectedTarget.commands.descriptorFront.isWebExtension) 297 ) { 298 return l10n.getStr("webconsole.input.selector.top"); 299 } 300 301 return selectedTarget.name; 302 } 303 304 render() { 305 const { webConsoleUI, targets, selectedTarget } = this.props; 306 307 // Don't render if there's only one target. 308 // Also bail out if the console is being destroyed (where WebConsoleUI.wrapper gets 309 // nullified). 310 if (targets.length <= 1 || !webConsoleUI.wrapper) { 311 return null; 312 } 313 314 const doc = webConsoleUI.document; 315 const { toolbox } = webConsoleUI.wrapper; 316 317 return MenuButton( 318 { 319 menuId: "webconsole-input-evaluationsButton", 320 toolboxDoc: toolbox ? toolbox.doc : doc, 321 label: this.getLabel(), 322 className: 323 "webconsole-evaluation-selector-button devtools-button devtools-dropdown-button" + 324 (selectedTarget && !selectedTarget.isTopLevel ? " checked" : ""), 325 title: l10n.getStr("webconsole.input.selector.tooltip"), 326 }, 327 // We pass the children in a function so we don't require the MenuItem and MenuList 328 // components until we need to display them (i.e. when the button is clicked). 329 () => this.renderMenuItems() 330 ); 331 } 332 } 333 334 const toolboxConnected = connect( 335 state => ({ 336 targets: targetSelectors.getToolboxTargets(state), 337 selectedTarget: targetSelectors.getSelectedTarget(state), 338 lastTargetRefresh: targetSelectors.getLastTargetRefresh(state), 339 }), 340 dispatch => ({ 341 selectTarget: actorID => dispatch(targetActions.selectTarget(actorID)), 342 }), 343 undefined, 344 { storeKey: "target-store" } 345 )(EvaluationContextSelector); 346 347 module.exports = connect( 348 state => state, 349 dispatch => ({ 350 onContextChange: () => { 351 dispatch( 352 webconsoleActions.updateInstantEvaluationResultForCurrentExpression() 353 ); 354 dispatch(webconsoleActions.autocompleteClear()); 355 }, 356 }) 357 )(toolboxConnected);