290 lines
9 KiB
JavaScript
290 lines
9 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.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;
|
|
|
|
const label = target.isTopLevel
|
|
? 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;
|
|
|
|
if (!selectedTarget || selectedTarget.isTopLevel) {
|
|
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);
|