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
"use strict";
const {
Component,
createFactory,
} = require("resource://devtools/client/shared/vendor/react.js");
loader.lazyRequireGetter(
this,
"PropTypes",
"resource://devtools/client/shared/vendor/react-prop-types.js"
);
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const {
connect,
} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
const actions = require("resource://devtools/client/webconsole/actions/index.js");
const {
FILTERBAR_DISPLAY_MODES,
} = require("resource://devtools/client/webconsole/constants.js");
// We directly require Components that we know are going to be used right away
const ConsoleOutput = createFactory(
require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
);
const FilterBar = createFactory(
require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js")
);
const ReverseSearchInput = createFactory(
require("resource://devtools/client/webconsole/components/Input/ReverseSearchInput.js")
);
const JSTerm = createFactory(
require("resource://devtools/client/webconsole/components/Input/JSTerm.js")
);
const ConfirmDialog = createFactory(
require("resource://devtools/client/webconsole/components/Input/ConfirmDialog.js")
);
const EagerEvaluation = createFactory(
require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js")
);
const EvaluationNotification = createFactory(
require("resource://devtools/client/webconsole/components/Input/EvaluationNotification.js")
);
// And lazy load the ones that may not be used.
loader.lazyGetter(this, "SideBar", () =>
createFactory(
require("resource://devtools/client/webconsole/components/SideBar.js")
)
);
loader.lazyGetter(this, "EditorToolbar", () =>
createFactory(
require("resource://devtools/client/webconsole/components/Input/EditorToolbar.js")
)
);
loader.lazyGetter(this, "NotificationBox", () =>
createFactory(
require("resource://devtools/client/shared/components/NotificationBox.js")
.NotificationBox
)
);
loader.lazyRequireGetter(
this,
["getNotificationWithValue", "PriorityLevels"],
"resource://devtools/client/shared/components/NotificationBox.js",
true
);
loader.lazyGetter(this, "GridElementWidthResizer", () =>
createFactory(
require("resource://devtools/client/shared/components/splitter/GridElementWidthResizer.js")
)
);
loader.lazyGetter(this, "ChromeDebugToolbar", () =>
createFactory(
require("resource://devtools/client/framework/components/ChromeDebugToolbar.js")
)
);
const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
const {
Utils: WebConsoleUtils,
} = require("resource://devtools/client/webconsole/utils.js");
const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
const {
getAllNotifications,
} = require("resource://devtools/client/webconsole/selectors/notifications.js");
const { div } = dom;
const isMacOS = Services.appinfo.OS === "Darwin";
/**
* Console root Application component.
*/
class App extends Component {
static get propTypes() {
return {
dispatch: PropTypes.func.isRequired,
webConsoleUI: PropTypes.object.isRequired,
notifications: PropTypes.object,
onFirstMeaningfulPaint: PropTypes.func.isRequired,
serviceContainer: PropTypes.object.isRequired,
closeSplitConsole: PropTypes.func.isRequired,
autocomplete: PropTypes.bool,
currentReverseSearchEntry: PropTypes.string,
reverseSearchInputVisible: PropTypes.bool,
reverseSearchInitialValue: PropTypes.string,
editorMode: PropTypes.bool,
editorWidth: PropTypes.number,
inputEnabled: PropTypes.bool,
sidebarVisible: PropTypes.bool.isRequired,
eagerEvaluationEnabled: PropTypes.bool.isRequired,
filterBarDisplayMode: PropTypes.oneOf([
...Object.values(FILTERBAR_DISPLAY_MODES),
]).isRequired,
showEvaluationContextSelector: PropTypes.bool,
};
}
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.onPaste = this.onPaste.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onBlur = this.onBlur.bind(this);
}
componentDidMount() {
window.addEventListener("blur", this.onBlur);
}
onBlur() {
this.props.dispatch(actions.autocompleteClear());
}
onKeyDown(event) {
const { dispatch, webConsoleUI } = this.props;
if (
(!isMacOS && event.key === "F9") ||
(isMacOS && event.key === "r" && event.ctrlKey === true)
) {
const initialValue =
webConsoleUI.jsterm && webConsoleUI.jsterm.getSelectedText();
dispatch(
actions.reverseSearchInputToggle({ initialValue, access: "keyboard" })
);
event.stopPropagation();
event.preventDefault();
}
if (
event.key.toLowerCase() === "b" &&
((isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey))
) {
event.stopPropagation();
event.preventDefault();
dispatch(actions.editorToggle());
}
}
onClick(event) {
const target = event.originalTarget || event.target;
const { reverseSearchInputVisible, dispatch, webConsoleUI } = this.props;
if (
reverseSearchInputVisible === true &&
!target.closest(".reverse-search")
) {
event.preventDefault();
event.stopPropagation();
dispatch(actions.reverseSearchInputToggle());
return;
}
// Do not focus on middle/right-click or 2+ clicks.
if (event.detail !== 1 || event.button !== 0) {
return;
}
// Do not focus if a link was clicked
if (target.closest("a")) {
return;
}
// Do not focus if an input field was clicked
if (target.closest("input")) {
return;
}
// Do not focus if the click happened in the reverse search toolbar.
if (target.closest(".reverse-search")) {
return;
}
// Do not focus if something other than the output region was clicked
// (including e.g. the clear messages button in toolbar)
if (!target.closest(".webconsole-app")) {
return;
}
// Do not focus if something is selected
const selection = webConsoleUI.document.defaultView.getSelection();
if (selection && !selection.isCollapsed) {
return;
}
if (webConsoleUI?.jsterm) {
webConsoleUI.jsterm.focus();
}
}
onPaste(event) {
const { dispatch, webConsoleUI, notifications } = this.props;
const { usageCount, CONSOLE_ENTRY_THRESHOLD } = WebConsoleUtils;
// Bail out if self-xss notification is suppressed.
if (
webConsoleUI.isBrowserConsole ||
usageCount >= CONSOLE_ENTRY_THRESHOLD
) {
return;
}
// Stop event propagation, so the clipboard content is *not* inserted.
event.preventDefault();
event.stopPropagation();
// Bail out if self-xss notification is already there.
if (getNotificationWithValue(notifications, "selfxss-notification")) {
return;
}
const input = event.target;
// Cleanup function if notification is closed by the user.
const removeCallback = eventType => {
if (eventType == "removed") {
input.removeEventListener("keyup", pasteKeyUpHandler);
dispatch(actions.removeNotification("selfxss-notification"));
}
};
// Create self-xss notification
dispatch(
actions.appendNotification(
SELF_XSS_MSG,
"selfxss-notification",
null,
PriorityLevels.PRIORITY_WARNING_HIGH,
null,
removeCallback
)
);
// Remove notification automatically when the user types "allow pasting".
const pasteKeyUpHandler = e => {
const { value } = e.target;
if (value.includes(SELF_XSS_OK)) {
dispatch(actions.removeNotification("selfxss-notification"));
input.removeEventListener("keyup", pasteKeyUpHandler);
WebConsoleUtils.usageCount = WebConsoleUtils.CONSOLE_ENTRY_THRESHOLD;
}
};
input.addEventListener("keyup", pasteKeyUpHandler);
}
renderChromeDebugToolbar() {
const { webConsoleUI } = this.props;
if (!webConsoleUI.isBrowserConsole) {
return null;
}
return ChromeDebugToolbar({
// This should always be true at this point
isBrowserConsole: webConsoleUI.isBrowserConsole,
});
}
renderFilterBar() {
const { closeSplitConsole, filterBarDisplayMode, webConsoleUI } =
this.props;
return FilterBar({
key: "filterbar",
closeSplitConsole,
displayMode: filterBarDisplayMode,
webConsoleUI,
});
}
renderEditorToolbar() {
const {
editorMode,
dispatch,
reverseSearchInputVisible,
serviceContainer,
webConsoleUI,
showEvaluationContextSelector,
inputEnabled,
} = this.props;
if (!inputEnabled) {
return null;
}
return editorMode
? EditorToolbar({
key: "editor-toolbar",
editorMode,
dispatch,
reverseSearchInputVisible,
serviceContainer,
showEvaluationContextSelector,
webConsoleUI,
})
: null;
}
renderConsoleOutput() {
const { onFirstMeaningfulPaint, serviceContainer, editorMode } = this.props;
return ConsoleOutput({
key: "console-output",
serviceContainer,
onFirstMeaningfulPaint,
editorMode,
});
}
renderJsTerm() {
const {
webConsoleUI,
serviceContainer,
autocomplete,
editorMode,
editorWidth,
inputEnabled,
} = this.props;
return JSTerm({
key: "jsterm",
webConsoleUI,
serviceContainer,
onPaste: this.onPaste,
autocomplete,
editorMode,
editorWidth,
inputEnabled,
});
}
renderEagerEvaluation() {
const { eagerEvaluationEnabled, serviceContainer, inputEnabled } =
this.props;
if (!eagerEvaluationEnabled || !inputEnabled) {
return null;
}
return EagerEvaluation({ serviceContainer });
}
renderReverseSearch() {
const { serviceContainer, reverseSearchInitialValue } = this.props;
return ReverseSearchInput({
key: "reverse-search-input",
setInputValue: serviceContainer.setInputValue,
focusInput: serviceContainer.focusInput,
initialValue: reverseSearchInitialValue,
});
}
renderSideBar() {
const { serviceContainer, sidebarVisible } = this.props;
return sidebarVisible
? SideBar({
key: "sidebar",
serviceContainer,
visible: sidebarVisible,
})
: null;
}
renderNotificationBox() {
const { notifications, editorMode } = this.props;
return notifications && notifications.size > 0
? NotificationBox({
id: "webconsole-notificationbox",
key: "notification-box",
displayBorderTop: !editorMode,
displayBorderBottom: editorMode,
wrapping: true,
notifications,
})
: null;
}
renderConfirmDialog() {
const { webConsoleUI, serviceContainer } = this.props;
return ConfirmDialog({
webConsoleUI,
serviceContainer,
key: "confirm-dialog",
});
}
renderRootElement(children) {
const { editorMode, sidebarVisible, inputEnabled, eagerEvaluationEnabled } =
this.props;
const classNames = ["webconsole-app"];
if (sidebarVisible) {
classNames.push("sidebar-visible");
}
if (editorMode && inputEnabled) {
classNames.push("jsterm-editor");
}
if (eagerEvaluationEnabled && inputEnabled) {
classNames.push("eager-evaluation");
}
return div(
{
className: classNames.join(" "),
onKeyDown: this.onKeyDown,
onClick: this.onClick,
ref: node => {
this.node = node;
},
},
children
);
}
render() {
const { webConsoleUI, editorMode, dispatch, inputEnabled } = this.props;
const chromeDebugToolbar = this.renderChromeDebugToolbar();
const filterBar = this.renderFilterBar();
const editorToolbar = this.renderEditorToolbar();
const consoleOutput = this.renderConsoleOutput();
const notificationBox = this.renderNotificationBox();
const jsterm = this.renderJsTerm();
const eager = this.renderEagerEvaluation();
const evaluationNotification = EvaluationNotification();
const reverseSearch = this.renderReverseSearch();
const sidebar = this.renderSideBar();
const confirmDialog = this.renderConfirmDialog();
return this.renderRootElement([
chromeDebugToolbar,
filterBar,
editorToolbar,
dom.div(
{ className: "flexible-output-input", key: "in-out-container" },
consoleOutput,
notificationBox,
jsterm,
eager,
evaluationNotification
),
editorMode && inputEnabled
? GridElementWidthResizer({
key: "editor-resizer",
enabled: editorMode,
position: "end",
className: "editor-resizer",
getControlledElementNode: () => webConsoleUI.jsterm.node,
onResizeEnd: width => dispatch(actions.setEditorWidth(width)),
})
: null,
reverseSearch,
sidebar,
confirmDialog,
]);
}
}
const mapStateToProps = state => ({
notifications: getAllNotifications(state),
reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
reverseSearchInitialValue: state.ui.reverseSearchInitialValue,
editorMode: state.ui.editor,
editorWidth: state.ui.editorWidth,
sidebarVisible: state.ui.sidebarVisible,
filterBarDisplayMode: state.ui.filterBarDisplayMode,
eagerEvaluationEnabled: state.prefs.eagerEvaluation,
autocomplete: state.prefs.autocomplete,
showEvaluationContextSelector: state.ui.showEvaluationContextSelector,
});
const mapDispatchToProps = dispatch => ({
dispatch,
});
module.exports = connect(mapStateToProps, mapDispatchToProps)(App);