diff options
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js')
-rw-r--r-- | devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js | 381 |
1 files changed, 381 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..0920fa0acf --- /dev/null +++ b/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js @@ -0,0 +1,381 @@ +/* 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/>. */ + +// @flow + +import React, { Component } from "react"; +import { connect } from "../../utils/connect"; +import classnames from "classnames"; +import actions from "../../actions"; + +import { CloseButton } from "../shared/Button"; + +import "./XHRBreakpoints.css"; +import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors"; +import ExceptionOption from "./Breakpoints/ExceptionOption"; + +import type { XHRBreakpointsList } from "../../reducers/types"; +import type { XHRBreakpoint } from "../../types"; + +type OwnProps = {| + onXHRAdded: () => void, + showInput: boolean, +|}; +type Props = { + xhrBreakpoints: XHRBreakpointsList, + shouldPauseOnAny: boolean, + showInput: boolean, + onXHRAdded: Function, + setXHRBreakpoint: Function, + removeXHRBreakpoint: typeof actions.removeXHRBreakpoint, + enableXHRBreakpoint: typeof actions.enableXHRBreakpoint, + disableXHRBreakpoint: typeof actions.disableXHRBreakpoint, + togglePauseOnAny: typeof actions.togglePauseOnAny, + updateXHRBreakpoint: typeof actions.updateXHRBreakpoint, +}; + +type State = { + editing: boolean, + inputValue: string, + inputMethod: string, + editIndex: number, + focused: boolean, + clickedOnFormElement: boolean, +}; + +// 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<Props, State> { + _input: ?HTMLInputElement; + + constructor(props: Props) { + super(props); + + this.state = { + editing: false, + inputValue: "", + inputMethod: "ANY", + focused: false, + editIndex: -1, + clickedOnFormElement: false, + }; + } + + 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: Props, prevState: State) { + 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: SyntheticEvent<HTMLFormElement>) => { + 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( + // $FlowIgnore + { inputMethod: e.target.children[1].value }, + setXHRBreakpoint + ); + }; + + handleExistingSubmit = (e: SyntheticEvent<HTMLFormElement>) => { + 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: SyntheticInputEvent<HTMLInputElement>) => { + this.setState({ inputValue: e.target.value }); + }; + + handleMethodChange = (e: SyntheticInputEvent<HTMLInputElement>) => { + 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: SyntheticEvent<HTMLElement>) => { + this.setState({ editing: false, clickedOnFormElement: true }); + }; + + handleTab = (e: SyntheticKeyboardEvent<HTMLElement>) => { + 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: number) => { + const { xhrBreakpoints } = this.props; + const { path, method } = xhrBreakpoints[index]; + this.setState({ + inputValue: path, + inputMethod: method, + editing: true, + editIndex: index, + }); + }; + + renderXHRInput(onSubmit: (e: SyntheticEvent<HTMLFormElement>) => void) { + 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" }} /> + </form> + ); + } + + handleCheckbox = (index: number) => { + const { + xhrBreakpoints, + enableXHRBreakpoint, + disableXHRBreakpoint, + } = this.props; + const breakpoint = xhrBreakpoints[index]; + if (breakpoint.disabled) { + enableXHRBreakpoint(index); + } else { + disableXHRBreakpoint(index); + } + }; + + renderBreakpoint = (breakpoint: XHRBreakpoint) => { + const { path, disabled, method } = breakpoint; + const { editIndex } = this.state; + const { removeXHRBreakpoint, xhrBreakpoints } = this.props; + + // The "pause on any" checkbox + if (!path) { + return; + } + + // 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> + <input + type="checkbox" + className="xhr-checkbox" + checked={!disabled} + onChange={() => this.handleCheckbox(index)} + onClick={ev => ev.stopPropagation()} + /> + <div className="xhr-label-method">{method}</div> + <div className="xhr-label-url">{path}</div> + <div className="xhr-container__close-btn"> + <CloseButton handleClick={e => removeXHRBreakpoint(index)} /> + </div> + </label> + </li> + ); + }; + + renderBreakpoints = (explicitXhrBreakpoints: XHRBreakpointsList) => { + const { showInput } = this.props; + + return ( + <> + <ul className="pane expressions-list"> + {explicitXhrBreakpoints.map(this.renderBreakpoint)} + </ul> + {showInput && this.renderXHRInput(this.handleNewSubmit)} + </> + ); + }; + + renderCheckbox = (explicitXhrBreakpoints: XHRBreakpointsList) => { + const { shouldPauseOnAny, togglePauseOnAny } = this.props; + + return ( + <div + className={classnames("breakpoints-exceptions-options", { + empty: explicitXhrBreakpoints.length === 0, + })} + > + <ExceptionOption + className="breakpoints-exceptions" + label={L10N.getStr("pauseOnAnyXHR")} + isChecked={shouldPauseOnAny} + onChange={() => togglePauseOnAny()} + /> + </div> + ); + }; + + renderMethodOption = (method: string) => { + 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} + </option> + ); + }; + + renderMethodSelectElement = () => { + return ( + <select + value={this.state.inputMethod} + className="xhr-input-method" + onChange={this.handleMethodChange} + onMouseDown={this.onMouseDown} + onKeyDown={this.handleTab} + > + {xhrMethods.map(this.renderMethodOption)} + </select> + ); + }; + + render() { + const { xhrBreakpoints } = this.props; + const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints); + + return ( + <> + {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<Props, OwnProps, _, _, _, _>(mapStateToProps, { + setXHRBreakpoint: actions.setXHRBreakpoint, + removeXHRBreakpoint: actions.removeXHRBreakpoint, + enableXHRBreakpoint: actions.enableXHRBreakpoint, + disableXHRBreakpoint: actions.disableXHRBreakpoint, + updateXHRBreakpoint: actions.updateXHRBreakpoint, + togglePauseOnAny: actions.togglePauseOnAny, +})(XHRBreakpoints); |