/* 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";
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();
// Prevent Reader Mode to be enabled (See Bug 1682340)
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);