diff options
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js')
-rw-r--r-- | devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js new file mode 100644 index 0000000000..9774255dcd --- /dev/null +++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js @@ -0,0 +1,383 @@ +/* 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/>. */ + +import React, { Component } from "devtools/client/shared/vendor/react"; +import { + div, + form, + input, + li, + label, + ul, + option, + select, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import { connect } from "devtools/client/shared/vendor/react-redux"; +import actions from "../../actions/index"; + +import { CloseButton } from "../shared/Button/index"; + +import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors/index"; +import ExceptionOption from "./Breakpoints/ExceptionOption"; + +const classnames = require("resource://devtools/client/shared/classnames.js"); + +// At present, the "Pause on any URL" checkbox creates an xhrBreakpoint +// of "ANY" with no path, so we can remove that before creating the list +function getExplicitXHRBreakpoints(xhrBreakpoints) { + return xhrBreakpoints.filter(bp => bp.path !== ""); +} + +const xhrMethods = [ + "ANY", + "GET", + "POST", + "PUT", + "HEAD", + "DELETE", + "PATCH", + "OPTIONS", +]; + +class XHRBreakpoints extends Component { + constructor(props) { + super(props); + + this.state = { + editing: false, + inputValue: "", + inputMethod: "ANY", + focused: false, + editIndex: -1, + clickedOnFormElement: false, + }; + } + + static get propTypes() { + return { + disableXHRBreakpoint: PropTypes.func.isRequired, + enableXHRBreakpoint: PropTypes.func.isRequired, + onXHRAdded: PropTypes.func.isRequired, + removeXHRBreakpoint: PropTypes.func.isRequired, + setXHRBreakpoint: PropTypes.func.isRequired, + shouldPauseOnAny: PropTypes.bool.isRequired, + showInput: PropTypes.bool.isRequired, + togglePauseOnAny: PropTypes.func.isRequired, + updateXHRBreakpoint: PropTypes.func.isRequired, + xhrBreakpoints: PropTypes.array.isRequired, + }; + } + + componentDidMount() { + const { showInput } = this.props; + + // Ensures that the input is focused when the "+" + // is clicked while the panel is collapsed + if (this._input && showInput) { + this._input.focus(); + } + } + + componentDidUpdate(prevProps, prevState) { + const _input = this._input; + + if (!_input) { + return; + } + + if (!prevState.editing && this.state.editing) { + _input.setSelectionRange(0, _input.value.length); + _input.focus(); + } else if (this.props.showInput && !this.state.focused) { + _input.focus(); + } + } + + handleNewSubmit = e => { + e.preventDefault(); + e.stopPropagation(); + + const setXHRBreakpoint = function () { + this.props.setXHRBreakpoint( + this.state.inputValue, + this.state.inputMethod + ); + this.hideInput(); + }; + + // force update inputMethod in state for mochitest purposes + // before setting XHR breakpoint + this.setState( + { inputMethod: e.target.children[1].value }, + setXHRBreakpoint + ); + }; + + handleExistingSubmit = e => { + e.preventDefault(); + e.stopPropagation(); + + const { editIndex, inputValue, inputMethod } = this.state; + const { xhrBreakpoints } = this.props; + const { path, method } = xhrBreakpoints[editIndex]; + + if (path !== inputValue || method != inputMethod) { + this.props.updateXHRBreakpoint(editIndex, inputValue, inputMethod); + } + + this.hideInput(); + }; + + handleChange = e => { + this.setState({ inputValue: e.target.value }); + }; + + handleMethodChange = e => { + this.setState({ + focused: true, + editing: true, + inputMethod: e.target.value, + }); + }; + + hideInput = () => { + if (this.state.clickedOnFormElement) { + this.setState({ + focused: true, + clickedOnFormElement: false, + }); + } else { + this.setState({ + focused: false, + editing: false, + editIndex: -1, + inputValue: "", + inputMethod: "ANY", + }); + this.props.onXHRAdded(); + } + }; + + onFocus = () => { + this.setState({ focused: true, editing: true }); + }; + + onMouseDown = e => { + this.setState({ editing: false, clickedOnFormElement: true }); + }; + + handleTab = e => { + if (e.key !== "Tab") { + return; + } + + if (e.currentTarget.nodeName === "INPUT") { + this.setState({ + clickedOnFormElement: true, + editing: false, + }); + } else if (e.currentTarget.nodeName === "SELECT" && !e.shiftKey) { + // The user has tabbed off the select and we should + // cancel the edit + this.hideInput(); + } + }; + + editExpression = index => { + const { xhrBreakpoints } = this.props; + const { path, method } = xhrBreakpoints[index]; + this.setState({ + inputValue: path, + inputMethod: method, + editing: true, + editIndex: index, + }); + }; + + renderXHRInput(onSubmit) { + const { focused, inputValue } = this.state; + const placeholder = L10N.getStr("xhrBreakpoints.placeholder"); + return form( + { + key: "xhr-input-container", + className: classnames("xhr-input-container xhr-input-form", { + focused, + }), + onSubmit: onSubmit, + }, + input({ + className: "xhr-input-url", + type: "text", + placeholder: placeholder, + onChange: this.handleChange, + onBlur: this.hideInput, + onFocus: this.onFocus, + value: inputValue, + onKeyDown: this.handleTab, + ref: c => (this._input = c), + }), + this.renderMethodSelectElement(), + input({ + type: "submit", + style: { + display: "none", + }, + }) + ); + } + + handleCheckbox = index => { + const { xhrBreakpoints, enableXHRBreakpoint, disableXHRBreakpoint } = + this.props; + const breakpoint = xhrBreakpoints[index]; + if (breakpoint.disabled) { + enableXHRBreakpoint(index); + } else { + disableXHRBreakpoint(index); + } + }; + + renderBreakpoint = breakpoint => { + const { path, disabled, method } = breakpoint; + const { editIndex } = this.state; + const { removeXHRBreakpoint, xhrBreakpoints } = this.props; + + // The "pause on any" checkbox + if (!path) { + return null; + } + + // Finds the xhrbreakpoint so as to not make assumptions about position + const index = xhrBreakpoints.findIndex( + bp => bp.path === path && bp.method === method + ); + + if (index === editIndex) { + return this.renderXHRInput(this.handleExistingSubmit); + } + return li( + { + className: "xhr-container", + key: `${path}-${method}`, + title: path, + onDoubleClick: (items, options) => this.editExpression(index), + }, + label( + null, + React.createElement("input", { + type: "checkbox", + className: "xhr-checkbox", + checked: !disabled, + onChange: () => this.handleCheckbox(index), + onClick: ev => ev.stopPropagation(), + }), + div( + { + className: "xhr-label-method", + }, + method + ), + div( + { + className: "xhr-label-url", + }, + path + ), + div( + { + className: "xhr-container__close-btn", + }, + React.createElement(CloseButton, { + handleClick: e => removeXHRBreakpoint(index), + }) + ) + ) + ); + }; + + renderBreakpoints = explicitXhrBreakpoints => { + const { showInput } = this.props; + return React.createElement( + React.Fragment, + null, + ul( + { + className: "pane expressions-list", + }, + explicitXhrBreakpoints.map(this.renderBreakpoint) + ), + showInput && this.renderXHRInput(this.handleNewSubmit) + ); + }; + + renderCheckbox = explicitXhrBreakpoints => { + const { shouldPauseOnAny, togglePauseOnAny } = this.props; + return div( + { + className: classnames("breakpoints-options", { + empty: explicitXhrBreakpoints.length === 0, + }), + }, + React.createElement(ExceptionOption, { + className: "breakpoints-exceptions", + label: L10N.getStr("pauseOnAnyXHR"), + isChecked: shouldPauseOnAny, + onChange: () => togglePauseOnAny(), + }) + ); + }; + renderMethodOption = method => { + return option( + { + key: method, + value: method, + // e.stopPropagation() required here since otherwise Firefox triggers 2x + // onMouseDown events on <select> upon clicking on an <option> + onMouseDown: e => e.stopPropagation(), + }, + method + ); + }; + + renderMethodSelectElement = () => { + return select( + { + value: this.state.inputMethod, + className: "xhr-input-method", + onChange: this.handleMethodChange, + onMouseDown: this.onMouseDown, + onKeyDown: this.handleTab, + }, + xhrMethods.map(this.renderMethodOption) + ); + }; + + render() { + const { xhrBreakpoints } = this.props; + const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints); + return React.createElement( + React.Fragment, + null, + this.renderCheckbox(explicitXhrBreakpoints), + explicitXhrBreakpoints.length === 0 + ? this.renderXHRInput(this.handleNewSubmit) + : this.renderBreakpoints(explicitXhrBreakpoints) + ); + } +} + +const mapStateToProps = state => ({ + xhrBreakpoints: getXHRBreakpoints(state), + shouldPauseOnAny: shouldPauseOnAnyXHR(state), +}); + +export default connect(mapStateToProps, { + setXHRBreakpoint: actions.setXHRBreakpoint, + removeXHRBreakpoint: actions.removeXHRBreakpoint, + enableXHRBreakpoint: actions.enableXHRBreakpoint, + disableXHRBreakpoint: actions.disableXHRBreakpoint, + updateXHRBreakpoint: actions.updateXHRBreakpoint, + togglePauseOnAny: actions.togglePauseOnAny, +})(XHRBreakpoints); |