summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/components/App.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/webconsole/components/App.js508
1 files changed, 508 insertions, 0 deletions
diff --git a/devtools/client/webconsole/components/App.js b/devtools/client/webconsole/components/App.js
new file mode 100644
index 0000000000..d09157a65a
--- /dev/null
+++ b/devtools/client/webconsole/components/App.js
@@ -0,0 +1,508 @@
+/* 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")
+);
+
+// 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 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
+ ),
+ 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);