1
0
Fork 0
firefox/devtools/client/webconsole/components/Input/EvaluationContextSelector.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

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);