/* 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 . */ // @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 { _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) => { 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) => { 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) => { this.setState({ inputValue: e.target.value }); }; handleMethodChange = (e: SyntheticInputEvent) => { 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) => { this.setState({ editing: false, clickedOnFormElement: true }); }; handleTab = (e: SyntheticKeyboardEvent) => { 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) => void) { const { focused, inputValue } = this.state; const placeholder = L10N.getStr("xhrBreakpoints.placeholder"); return ( (this._input = c)} /> {this.renderMethodSelectElement()} ); } 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 ( this.editExpression(index)} > this.handleCheckbox(index)} onClick={ev => ev.stopPropagation()} /> {method} {path} removeXHRBreakpoint(index)} /> ); }; renderBreakpoints = (explicitXhrBreakpoints: XHRBreakpointsList) => { const { showInput } = this.props; return ( <> {explicitXhrBreakpoints.map(this.renderBreakpoint)} {showInput && this.renderXHRInput(this.handleNewSubmit)} > ); }; renderCheckbox = (explicitXhrBreakpoints: XHRBreakpointsList) => { const { shouldPauseOnAny, togglePauseOnAny } = this.props; return ( togglePauseOnAny()} /> ); }; renderMethodOption = (method: string) => { return ( upon clicking on an onMouseDown={e => e.stopPropagation()} > {method} ); }; renderMethodSelectElement = () => { return ( {xhrMethods.map(this.renderMethodOption)} ); }; 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(mapStateToProps, { setXHRBreakpoint: actions.setXHRBreakpoint, removeXHRBreakpoint: actions.removeXHRBreakpoint, enableXHRBreakpoint: actions.enableXHRBreakpoint, disableXHRBreakpoint: actions.disableXHRBreakpoint, updateXHRBreakpoint: actions.updateXHRBreakpoint, togglePauseOnAny: actions.togglePauseOnAny, })(XHRBreakpoints);