diff options
Diffstat (limited to 'devtools/client/webconsole/components/App.js')
-rw-r--r-- | devtools/client/webconsole/components/App.js | 508 |
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); |