357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
/* 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);
|