forked from mirrors/gecko-dev
		
	We weren't calling preventDefault on the event handler, which was allowing the browser to consume it, and toggle reader mode on windows, which shares the same keyboard shortcut (F9). A test is added to ensure we don't regress this (the test is failing without the fix). Differential Revision: https://phabricator.services.mozilla.com/D101919
		
			
				
	
	
		
			501 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			501 lines
		
	
	
	
		
			13 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";
 | 
						|
 | 
						|
const Services = require("Services");
 | 
						|
const {
 | 
						|
  Component,
 | 
						|
  createFactory,
 | 
						|
} = require("devtools/client/shared/vendor/react");
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  "PropTypes",
 | 
						|
  "devtools/client/shared/vendor/react-prop-types"
 | 
						|
);
 | 
						|
const dom = require("devtools/client/shared/vendor/react-dom-factories");
 | 
						|
const {
 | 
						|
  connect,
 | 
						|
} = require("devtools/client/shared/redux/visibility-handler-connect");
 | 
						|
 | 
						|
const actions = require("devtools/client/webconsole/actions/index");
 | 
						|
const {
 | 
						|
  FILTERBAR_DISPLAY_MODES,
 | 
						|
} = require("devtools/client/webconsole/constants");
 | 
						|
 | 
						|
// We directly require Components that we know are going to be used right away
 | 
						|
const ConsoleOutput = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/Output/ConsoleOutput")
 | 
						|
);
 | 
						|
const FilterBar = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/FilterBar/FilterBar")
 | 
						|
);
 | 
						|
const ReverseSearchInput = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/Input/ReverseSearchInput")
 | 
						|
);
 | 
						|
const JSTerm = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/Input/JSTerm")
 | 
						|
);
 | 
						|
const ConfirmDialog = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/Input/ConfirmDialog")
 | 
						|
);
 | 
						|
const EagerEvaluation = createFactory(
 | 
						|
  require("devtools/client/webconsole/components/Input/EagerEvaluation")
 | 
						|
);
 | 
						|
 | 
						|
// And lazy load the ones that may not be used.
 | 
						|
loader.lazyGetter(this, "SideBar", () =>
 | 
						|
  createFactory(require("devtools/client/webconsole/components/SideBar"))
 | 
						|
);
 | 
						|
 | 
						|
loader.lazyGetter(this, "EditorToolbar", () =>
 | 
						|
  createFactory(
 | 
						|
    require("devtools/client/webconsole/components/Input/EditorToolbar")
 | 
						|
  )
 | 
						|
);
 | 
						|
 | 
						|
loader.lazyGetter(this, "NotificationBox", () =>
 | 
						|
  createFactory(
 | 
						|
    require("devtools/client/shared/components/NotificationBox").NotificationBox
 | 
						|
  )
 | 
						|
);
 | 
						|
loader.lazyRequireGetter(
 | 
						|
  this,
 | 
						|
  ["getNotificationWithValue", "PriorityLevels"],
 | 
						|
  "devtools/client/shared/components/NotificationBox",
 | 
						|
  true
 | 
						|
);
 | 
						|
 | 
						|
loader.lazyGetter(this, "GridElementWidthResizer", () =>
 | 
						|
  createFactory(
 | 
						|
    require("devtools/client/shared/components/splitter/GridElementWidthResizer")
 | 
						|
  )
 | 
						|
);
 | 
						|
 | 
						|
const l10n = require("devtools/client/webconsole/utils/l10n");
 | 
						|
const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
 | 
						|
 | 
						|
const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
 | 
						|
const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
 | 
						|
 | 
						|
const {
 | 
						|
  getAllNotifications,
 | 
						|
} = require("devtools/client/webconsole/selectors/notifications");
 | 
						|
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,
 | 
						|
      hidePersistLogsCheckbox: PropTypes.bool,
 | 
						|
      hideShowContentMessagesCheckbox: PropTypes.bool,
 | 
						|
      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);
 | 
						|
  }
 | 
						|
 | 
						|
  renderFilterBar() {
 | 
						|
    const {
 | 
						|
      closeSplitConsole,
 | 
						|
      filterBarDisplayMode,
 | 
						|
      hidePersistLogsCheckbox,
 | 
						|
      hideShowContentMessagesCheckbox,
 | 
						|
      webConsoleUI,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    return FilterBar({
 | 
						|
      key: "filterbar",
 | 
						|
      hidePersistLogsCheckbox,
 | 
						|
      hideShowContentMessagesCheckbox,
 | 
						|
      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 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([
 | 
						|
      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);
 |