summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js')
-rw-r--r--devtools/client/debugger/src/components/SecondaryPanes/XHRBreakpoints.js381
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);