Source code

Revision control

Copy as Markdown

Other Tools

/* 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.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
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";
}
return null;
}
renderMenuItem(target) {
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",
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));
let mainTarget;
const sections = {
[TARGET_TYPES.FRAME]: [],
[TARGET_TYPES.WORKER]: [],
[TARGET_TYPES.SHARED_WORKER]: [],
[TARGET_TYPES.SERVICE_WORKER]: [],
};
// 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 |
// |------------------------------|
//
// This object will be keyed by PID, and each property will be an object with a
// `process` property (for the process target item), and a `frames` property (and array
// for all the frame target items).
const processes = {};
const { webConsoleUI } = this.props;
const handleProcessTargets =
webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole;
for (const target of targets) {
const menuItem = this.renderMenuItem(target);
if (target.isTopLevel) {
mainTarget = menuItem;
} else if (target.targetType == TARGET_TYPES.PROCESS) {
if (!processes[target.processID]) {
processes[target.processID] = { frames: [] };
}
processes[target.processID].process = menuItem;
} else if (
target.targetType == TARGET_TYPES.FRAME &&
handleProcessTargets &&
target.processID
) {
// The associated process target might not have been handled yet, so make sure
// to create it.
if (!processes[target.processID]) {
processes[target.processID] = { frames: [] };
}
processes[target.processID].frames.push(menuItem);
} else {
sections[target.targetType].push(menuItem);
}
}
// 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
const items = mainTarget ? [mainTarget] : [];
// Handle PROCESS targets sections first, as we want to display the associated frames
// below the process to group them.
if (processes) {
for (const [pid, { process, frames }] of Object.entries(processes)) {
items.push(dom.hr({ role: "menuseparator", key: `${pid}-separator` }));
if (process) {
items.push(process);
}
if (frames) {
items.push(...frames);
}
}
}
for (const [targetType, menuItems] of Object.entries(sections)) {
if (menuItems.length) {
items.push(
dom.hr({ role: "menuseparator", key: `${targetType}-separator` }),
...menuItems
);
}
}
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);