/* 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"; // React & Redux const { Component, createFactory, } = require("resource://devtools/client/shared/vendor/react.mjs"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); const { connect, } = require("resource://devtools/client/shared/vendor/react-redux.js"); const targetActions = require("resource://devtools/shared/commands/target/actions/targets.js"); const webconsoleActions = require("resource://devtools/client/webconsole/actions/index.js"); const { l10n, } = require("resource://devtools/client/webconsole/utils/messages.js"); const targetSelectors = require("resource://devtools/shared/commands/target/selectors/targets.js"); loader.lazyGetter(this, "TARGET_TYPES", function () { return require("resource://devtools/shared/commands/target/target-command.js") .TYPES; }); // Additional Components const MenuButton = createFactory( require("resource://devtools/client/shared/components/menu/MenuButton.js") ); loader.lazyGetter(this, "MenuItem", function () { return createFactory( require("resource://devtools/client/shared/components/menu/MenuItem.js") ); }); loader.lazyGetter(this, "MenuList", function () { return createFactory( require("resource://devtools/client/shared/components/menu/MenuList.js") ); }); class EvaluationContextSelector extends Component { static get propTypes() { return { selectTarget: PropTypes.func.isRequired, onContextChange: PropTypes.func.isRequired, selectedTarget: PropTypes.object, lastTargetRefresh: PropTypes.number, targets: PropTypes.array, webConsoleUI: PropTypes.object.isRequired, }; } shouldComponentUpdate(nextProps) { if (this.props.selectedTarget !== nextProps.selectedTarget) { return true; } if (this.props.lastTargetRefresh !== nextProps.lastTargetRefresh) { return true; } if (this.props.targets.length !== nextProps.targets.length) { return true; } for (let i = 0; i < nextProps.targets.length; i++) { const target = this.props.targets[i]; const nextTarget = nextProps.targets[i]; if (target.url != nextTarget.url || target.name != nextTarget.name) { return true; } } return false; } componentDidUpdate(prevProps) { if (this.props.selectedTarget !== prevProps.selectedTarget) { this.props.onContextChange(); } } getIcon(target) { if (target.targetType === TARGET_TYPES.FRAME) { return "chrome://devtools/content/debugger/images/globe-small.svg"; } if ( target.targetType === TARGET_TYPES.WORKER || target.targetType === TARGET_TYPES.SHARED_WORKER || target.targetType === TARGET_TYPES.SERVICE_WORKER ) { return "chrome://devtools/content/debugger/images/worker.svg"; } if (target.targetType === TARGET_TYPES.PROCESS) { return "chrome://devtools/content/debugger/images/window.svg"; } if (target.targetType === TARGET_TYPES.CONTENT_SCRIPT) { return "chrome://devtools/content/debugger/images/sources/extension.svg"; } return null; } renderMenuItem(target, indented = false) { const { selectTarget, selectedTarget } = this.props; // When debugging a Web Extension, the top level target is always the fallback document. // It isn't really a top level document as it won't be the parent of any other. // So only print its name. const label = target.isTopLevel && !target.commands.descriptorFront.isWebExtension ? l10n.getStr("webconsole.input.selector.top") : target.name; return MenuItem({ key: `webconsole-evaluation-selector-item-${target.actorID}`, className: `menu-item webconsole-evaluation-selector-item ${ indented ? "indented" : "" }`, type: "checkbox", checked: selectedTarget ? selectedTarget == target : target.isTopLevel, label, tooltip: target.url || target.name, icon: this.getIcon(target), onClick: () => selectTarget(target.actorID), }); } renderMenuItems() { const { targets } = this.props; // Let's sort the targets (using "numeric" so Content processes are ordered by PID). const collator = new Intl.Collator("en", { numeric: true }); targets.sort((a, b) => collator.compare(a.name, b.name)); // When in Browser Toolbox, we want to display the process targets with the frames // in the same process as a group // e.g. // |------------------------------| // | Top | // | -----------------------------| // | (pid 1234) priviledgedabout | // | New Tab | // | -----------------------------| // | (pid 5678) web | // | cnn.com | // | -----------------------------| // | RemoteSettingWorker.js | // |------------------------------| // const { webConsoleUI } = this.props; const handleProcessTargets = webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole; const processTargets = []; const frameTargets = new Set(); const contentScriptTargets = new Set(); const workerTargets = new Set(); let topTarget = null; for (const target of targets) { if (target.isTopLevel) { topTarget = target; continue; } switch (target.targetType) { case TARGET_TYPES.PROCESS: processTargets.push(target); break; case TARGET_TYPES.FRAME: frameTargets.add(target); break; case TARGET_TYPES.CONTENT_SCRIPT: contentScriptTargets.add(target); break; case TARGET_TYPES.WORKER: case TARGET_TYPES.SHARED_WORKER: case TARGET_TYPES.SERVICE_WORKER: workerTargets.add(target); break; default: console.warn( "Unsupported target type in the evalutiong context selector", target.targetType ); } } const items = []; const renderFrameWithContentScripts = frameTarget => { items.push(this.renderMenuItem(frameTarget)); // Render under each frame, its related web extension content scripts,... for (const contentScriptTarget of contentScriptTargets) { if (contentScriptTarget.innerWindowId != frameTarget.innerWindowId) { continue; } items.push(this.renderMenuItem(contentScriptTarget, true)); contentScriptTargets.delete(contentScriptTarget); } // ...as well as all its related workers for (const workerTarget of workerTargets) { if ( workerTarget.relatedDocumentInnerWindowId != frameTarget.innerWindowId ) { continue; } items.push(this.renderMenuItem(workerTarget, true)); workerTargets.delete(workerTarget); } }; // Note that while debugging popups, we might have a small period // of time where we don't have any top level target when we reload // the original tab if (topTarget) { renderFrameWithContentScripts(topTarget); } if (handleProcessTargets) { const sortedProcessTargets = processTargets.sort( (a, b) => a.processID < b.processID ); for (const target of sortedProcessTargets) { items.push( dom.hr({ role: "menuseparator", key: `process-separator-${target.actorID}`, }), this.renderMenuItem(target) ); for (const frameTarget of frameTargets) { if (frameTarget.processID != target.processID) { continue; } renderFrameWithContentScripts(frameTarget); frameTargets.delete(frameTarget); } } } // Render all targets when running in regular non-browser-console/toolbox, // but also possibly render any leftover frame which can't be matched to any Process ID. const sortedFrames = [...frameTargets].sort( (a, b) => a.innerWindowID < b.innerWindowID ); if (sortedFrames.length) { items.push(dom.hr({ role: "menuseparator", key: `frame-separator` })); } for (const frameTarget of sortedFrames) { renderFrameWithContentScripts(frameTarget); } // All content scripts and workers should have matched their related frame target in `renderFrameWithContentScripts`, // but just in case, display any leftover. for (const contentScriptTarget of contentScriptTargets) { items.push(this.renderMenuItem(contentScriptTarget)); } const sortedWorkers = [...workerTargets].sort((a, b) => a.url < b.url); if (sortedWorkers.length) { items.push(dom.hr({ role: "menuseparator", key: `worker-separator` })); } for (const workerTarget of sortedWorkers) { items.push(this.renderMenuItem(workerTarget)); } return MenuList( { id: "webconsole-console-evaluation-context-selector-menu-list" }, items ); } getLabel() { const { selectedTarget } = this.props; // When debugging a Web Extension, the top level target is always the fallback document. // It isn't really a top level document as it won't be the parent of any other. // So only print its name. if ( !selectedTarget || (selectedTarget.isTopLevel && !selectedTarget.commands.descriptorFront.isWebExtension) ) { return l10n.getStr("webconsole.input.selector.top"); } return selectedTarget.name; } render() { const { webConsoleUI, targets, selectedTarget } = this.props; // Don't render if there's only one target. // Also bail out if the console is being destroyed (where WebConsoleUI.wrapper gets // nullified). if (targets.length <= 1 || !webConsoleUI.wrapper) { return null; } const doc = webConsoleUI.document; const { toolbox } = webConsoleUI.wrapper; return MenuButton( { menuId: "webconsole-input-evaluationsButton", toolboxDoc: toolbox ? toolbox.doc : doc, label: this.getLabel(), className: "webconsole-evaluation-selector-button devtools-button devtools-dropdown-button" + (selectedTarget && !selectedTarget.isTopLevel ? " checked" : ""), title: l10n.getStr("webconsole.input.selector.tooltip"), }, // We pass the children in a function so we don't require the MenuItem and MenuList // components until we need to display them (i.e. when the button is clicked). () => this.renderMenuItems() ); } } const toolboxConnected = connect( state => ({ targets: targetSelectors.getToolboxTargets(state), selectedTarget: targetSelectors.getSelectedTarget(state), lastTargetRefresh: targetSelectors.getLastTargetRefresh(state), }), dispatch => ({ selectTarget: actorID => dispatch(targetActions.selectTarget(actorID)), }), undefined, { storeKey: "target-store" } )(EvaluationContextSelector); module.exports = connect( state => state, dispatch => ({ onContextChange: () => { dispatch( webconsoleActions.updateInstantEvaluationResultForCurrentExpression() ); dispatch(webconsoleActions.autocompleteClear()); }, }) )(toolboxConnected);