XHRBreakpoints.js (10037B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import React, { Component } from "devtools/client/shared/vendor/react"; 6 import { 7 div, 8 form, 9 input, 10 li, 11 label, 12 ul, 13 option, 14 select, 15 } from "devtools/client/shared/vendor/react-dom-factories"; 16 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 17 import { connect } from "devtools/client/shared/vendor/react-redux"; 18 import actions from "../../actions/index"; 19 20 import { CloseButton } from "../shared/Button/index"; 21 22 import { getXHRBreakpoints, shouldPauseOnAnyXHR } from "../../selectors/index"; 23 import ExceptionOption from "./Breakpoints/ExceptionOption"; 24 25 const classnames = require("resource://devtools/client/shared/classnames.js"); 26 27 // At present, the "Pause on any URL" checkbox creates an xhrBreakpoint 28 // of "ANY" with no path, so we can remove that before creating the list 29 function getExplicitXHRBreakpoints(xhrBreakpoints) { 30 return xhrBreakpoints.filter(bp => bp.path !== ""); 31 } 32 33 const xhrMethods = [ 34 "ANY", 35 "GET", 36 "POST", 37 "PUT", 38 "HEAD", 39 "DELETE", 40 "PATCH", 41 "OPTIONS", 42 ]; 43 44 class XHRBreakpoints extends Component { 45 constructor(props) { 46 super(props); 47 48 this.state = { 49 editing: false, 50 inputValue: "", 51 inputMethod: "ANY", 52 focused: false, 53 editIndex: -1, 54 clickedOnFormElement: false, 55 }; 56 } 57 58 static get propTypes() { 59 return { 60 disableXHRBreakpoint: PropTypes.func.isRequired, 61 enableXHRBreakpoint: PropTypes.func.isRequired, 62 onXHRAdded: PropTypes.func.isRequired, 63 removeXHRBreakpoint: PropTypes.func.isRequired, 64 setXHRBreakpoint: PropTypes.func.isRequired, 65 shouldPauseOnAny: PropTypes.bool.isRequired, 66 showInput: PropTypes.bool.isRequired, 67 togglePauseOnAny: PropTypes.func.isRequired, 68 updateXHRBreakpoint: PropTypes.func.isRequired, 69 xhrBreakpoints: PropTypes.array.isRequired, 70 }; 71 } 72 73 componentDidMount() { 74 const { showInput } = this.props; 75 76 // Ensures that the input is focused when the "+" 77 // is clicked while the panel is collapsed 78 if (this._input && showInput) { 79 this._input.focus(); 80 } 81 } 82 83 componentDidUpdate(prevProps, prevState) { 84 const _input = this._input; 85 86 if (!_input) { 87 return; 88 } 89 90 if (!prevState.editing && this.state.editing) { 91 _input.setSelectionRange(0, _input.value.length); 92 _input.focus(); 93 } else if (this.props.showInput && !this.state.focused) { 94 _input.focus(); 95 } 96 } 97 98 handleNewSubmit = e => { 99 e.preventDefault(); 100 e.stopPropagation(); 101 102 // Prevent adding breakpoint with empty path 103 if (!this.state.inputValue.trim()) { 104 return; 105 } 106 107 const setXHRBreakpoint = function () { 108 this.props.setXHRBreakpoint( 109 this.state.inputValue, 110 this.state.inputMethod 111 ); 112 this.hideInput(); 113 }; 114 115 // force update inputMethod in state for mochitest purposes 116 // before setting XHR breakpoint 117 this.setState( 118 { inputMethod: e.target.children[1].value }, 119 setXHRBreakpoint 120 ); 121 }; 122 123 handleExistingSubmit = e => { 124 e.preventDefault(); 125 e.stopPropagation(); 126 127 const { editIndex, inputValue, inputMethod } = this.state; 128 const { xhrBreakpoints } = this.props; 129 const { path, method } = xhrBreakpoints[editIndex]; 130 131 if (path !== inputValue || method != inputMethod) { 132 this.props.updateXHRBreakpoint(editIndex, inputValue, inputMethod); 133 } 134 135 this.hideInput(); 136 }; 137 138 handleChange = e => { 139 this.setState({ inputValue: e.target.value }); 140 }; 141 142 handleMethodChange = e => { 143 this.setState({ 144 focused: true, 145 editing: true, 146 inputMethod: e.target.value, 147 }); 148 }; 149 150 hideInput = () => { 151 if (this.state.clickedOnFormElement) { 152 this.setState({ 153 focused: true, 154 clickedOnFormElement: false, 155 }); 156 } else { 157 this.setState({ 158 focused: false, 159 editing: false, 160 editIndex: -1, 161 inputValue: "", 162 inputMethod: "ANY", 163 }); 164 this.props.onXHRAdded(); 165 } 166 }; 167 168 onFocus = () => { 169 this.setState({ focused: true, editing: true }); 170 }; 171 172 onMouseDown = () => { 173 this.setState({ editing: false, clickedOnFormElement: true }); 174 }; 175 176 handleTab = e => { 177 if (e.key !== "Tab") { 178 return; 179 } 180 181 if (e.currentTarget.nodeName === "INPUT") { 182 this.setState({ 183 clickedOnFormElement: true, 184 editing: false, 185 }); 186 } else if (e.currentTarget.nodeName === "SELECT" && !e.shiftKey) { 187 // The user has tabbed off the select and we should 188 // cancel the edit 189 this.hideInput(); 190 } 191 }; 192 193 editExpression = index => { 194 const { xhrBreakpoints } = this.props; 195 const { path, method } = xhrBreakpoints[index]; 196 this.setState({ 197 inputValue: path, 198 inputMethod: method, 199 editing: true, 200 editIndex: index, 201 }); 202 }; 203 204 renderXHRInput(onSubmit) { 205 const { focused, inputValue } = this.state; 206 const placeholder = L10N.getStr("xhrBreakpoints.placeholder"); 207 return form( 208 { 209 key: "xhr-input-container", 210 className: classnames("xhr-input-container xhr-input-form", { 211 focused, 212 }), 213 onSubmit, 214 }, 215 input({ 216 className: "xhr-input-url", 217 type: "text", 218 placeholder, 219 onChange: this.handleChange, 220 onBlur: this.hideInput, 221 onFocus: this.onFocus, 222 value: inputValue, 223 onKeyDown: this.handleTab, 224 ref: c => (this._input = c), 225 }), 226 this.renderMethodSelectElement(), 227 input({ 228 type: "submit", 229 style: { 230 display: "none", 231 }, 232 }) 233 ); 234 } 235 236 handleCheckbox = index => { 237 const { xhrBreakpoints, enableXHRBreakpoint, disableXHRBreakpoint } = 238 this.props; 239 const breakpoint = xhrBreakpoints[index]; 240 if (breakpoint.disabled) { 241 enableXHRBreakpoint(index); 242 } else { 243 disableXHRBreakpoint(index); 244 } 245 }; 246 247 renderBreakpoint = breakpoint => { 248 const { path, disabled, method } = breakpoint; 249 const { editIndex } = this.state; 250 const { removeXHRBreakpoint, xhrBreakpoints } = this.props; 251 252 // The "pause on any" checkbox 253 if (!path) { 254 return null; 255 } 256 257 // Finds the xhrbreakpoint so as to not make assumptions about position 258 const index = xhrBreakpoints.findIndex( 259 bp => bp.path === path && bp.method === method 260 ); 261 262 if (index === editIndex) { 263 return this.renderXHRInput(this.handleExistingSubmit); 264 } 265 return li( 266 { 267 className: "xhr-container", 268 key: `${path}-${method}`, 269 title: path, 270 onDoubleClick: () => this.editExpression(index), 271 }, 272 label( 273 null, 274 React.createElement("input", { 275 type: "checkbox", 276 className: "xhr-checkbox", 277 checked: !disabled, 278 onChange: () => this.handleCheckbox(index), 279 onClick: ev => ev.stopPropagation(), 280 }), 281 div( 282 { 283 className: "xhr-label-method", 284 }, 285 method 286 ), 287 div( 288 { 289 className: "xhr-label-url", 290 }, 291 path 292 ), 293 div( 294 { 295 className: "xhr-container__close-btn", 296 }, 297 React.createElement(CloseButton, { 298 handleClick: () => removeXHRBreakpoint(index), 299 }) 300 ) 301 ) 302 ); 303 }; 304 305 renderBreakpoints = explicitXhrBreakpoints => { 306 const { showInput } = this.props; 307 return React.createElement( 308 React.Fragment, 309 null, 310 ul( 311 { 312 className: "pane expressions-list", 313 }, 314 explicitXhrBreakpoints.map(this.renderBreakpoint) 315 ), 316 showInput && this.renderXHRInput(this.handleNewSubmit) 317 ); 318 }; 319 320 renderCheckbox = explicitXhrBreakpoints => { 321 const { shouldPauseOnAny, togglePauseOnAny } = this.props; 322 return div( 323 { 324 className: classnames("breakpoints-options", { 325 empty: explicitXhrBreakpoints.length === 0, 326 }), 327 }, 328 React.createElement(ExceptionOption, { 329 className: "breakpoints-exceptions", 330 label: L10N.getStr("pauseOnAnyXHR"), 331 isChecked: shouldPauseOnAny, 332 onChange: () => togglePauseOnAny(), 333 }) 334 ); 335 }; 336 renderMethodOption = method => { 337 return option( 338 { 339 key: method, 340 value: method, 341 // e.stopPropagation() required here since otherwise Firefox triggers 2x 342 // onMouseDown events on <select> upon clicking on an <option> 343 onMouseDown: e => e.stopPropagation(), 344 }, 345 method 346 ); 347 }; 348 349 renderMethodSelectElement = () => { 350 return select( 351 { 352 value: this.state.inputMethod, 353 className: "xhr-input-method", 354 onChange: this.handleMethodChange, 355 onMouseDown: this.onMouseDown, 356 onKeyDown: this.handleTab, 357 }, 358 xhrMethods.map(this.renderMethodOption) 359 ); 360 }; 361 362 render() { 363 const { xhrBreakpoints } = this.props; 364 const explicitXhrBreakpoints = getExplicitXHRBreakpoints(xhrBreakpoints); 365 return React.createElement( 366 React.Fragment, 367 null, 368 this.renderCheckbox(explicitXhrBreakpoints), 369 explicitXhrBreakpoints.length === 0 370 ? this.renderXHRInput(this.handleNewSubmit) 371 : this.renderBreakpoints(explicitXhrBreakpoints) 372 ); 373 } 374 } 375 376 const mapStateToProps = state => ({ 377 xhrBreakpoints: getXHRBreakpoints(state), 378 shouldPauseOnAny: shouldPauseOnAnyXHR(state), 379 }); 380 381 export default connect(mapStateToProps, { 382 setXHRBreakpoint: actions.setXHRBreakpoint, 383 removeXHRBreakpoint: actions.removeXHRBreakpoint, 384 enableXHRBreakpoint: actions.enableXHRBreakpoint, 385 disableXHRBreakpoint: actions.disableXHRBreakpoint, 386 updateXHRBreakpoint: actions.updateXHRBreakpoint, 387 togglePauseOnAny: actions.togglePauseOnAny, 388 })(XHRBreakpoints);