Expressions.js (12539B)
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 input, 9 li, 10 ul, 11 form, 12 datalist, 13 option, 14 span, 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 { features } from "../../utils/prefs"; 19 import DebuggerImage from "../shared/DebuggerImage"; 20 21 import * as objectInspector from "resource://devtools/client/shared/components/object-inspector/index.js"; 22 23 import actions from "../../actions/index"; 24 import { 25 getExpressions, 26 getAutocompleteMatchset, 27 getSelectedSource, 28 isMapScopesEnabled, 29 getIsCurrentThreadPaused, 30 getSelectedFrame, 31 getOriginalFrameScope, 32 } from "../../selectors/index"; 33 import { getExpressionResultGripAndFront } from "../../utils/expressions"; 34 35 import { CloseButton } from "../shared/Button/index"; 36 37 const { debounce } = require("resource://devtools/shared/debounce.js"); 38 39 const { ObjectInspector } = objectInspector; 40 41 class Expressions extends Component { 42 constructor(props) { 43 super(props); 44 45 this.state = { 46 editing: false, 47 editIndex: -1, 48 inputValue: "", 49 }; 50 } 51 52 static get propTypes() { 53 return { 54 addExpression: PropTypes.func.isRequired, 55 autocomplete: PropTypes.func.isRequired, 56 autocompleteMatches: PropTypes.array, 57 clearAutocomplete: PropTypes.func.isRequired, 58 deleteExpression: PropTypes.func.isRequired, 59 expressions: PropTypes.array.isRequired, 60 highlightDomElement: PropTypes.func.isRequired, 61 onExpressionAdded: PropTypes.func.isRequired, 62 openElementInInspector: PropTypes.func.isRequired, 63 openLink: PropTypes.any.isRequired, 64 showInput: PropTypes.bool.isRequired, 65 unHighlightDomElement: PropTypes.func.isRequired, 66 updateExpression: PropTypes.func.isRequired, 67 isOriginalVariableMappingDisabled: PropTypes.bool, 68 isLoadingOriginalVariables: PropTypes.bool, 69 }; 70 } 71 72 componentDidMount() { 73 const { showInput } = this.props; 74 75 // Ensures that the input is focused when the "+" 76 // is clicked while the panel is collapsed 77 if (showInput && this._input) { 78 this._input.focus(); 79 } 80 } 81 82 clear = () => { 83 this.setState(() => ({ 84 editing: false, 85 editIndex: -1, 86 inputValue: "", 87 })); 88 }; 89 90 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 91 UNSAFE_componentWillReceiveProps(nextProps) { 92 if (this.state.editing) { 93 this.clear(); 94 } 95 96 // Ensures that the add watch expression input 97 // is no longer visible when the new watch expression is rendered 98 if (this.props.expressions.length < nextProps.expressions.length) { 99 this.hideInput(); 100 } 101 } 102 103 shouldComponentUpdate(nextProps, nextState) { 104 const { editing, inputValue } = this.state; 105 const { 106 expressions, 107 showInput, 108 autocompleteMatches, 109 isLoadingOriginalVariables, 110 isOriginalVariableMappingDisabled, 111 } = this.props; 112 113 return ( 114 autocompleteMatches !== nextProps.autocompleteMatches || 115 expressions !== nextProps.expressions || 116 isLoadingOriginalVariables !== nextProps.isLoadingOriginalVariables || 117 isOriginalVariableMappingDisabled !== 118 nextProps.isOriginalVariableMappingDisabled || 119 editing !== nextState.editing || 120 inputValue !== nextState.inputValue || 121 nextProps.showInput !== showInput 122 ); 123 } 124 125 componentDidUpdate(prevProps, prevState) { 126 const _input = this._input; 127 128 if (!_input) { 129 return; 130 } 131 132 if (!prevState.editing && this.state.editing) { 133 _input.setSelectionRange(0, _input.value.length); 134 _input.focus(); 135 } else if (this.props.showInput) { 136 _input.focus(); 137 } 138 } 139 140 editExpression(expression, index) { 141 this.setState({ 142 inputValue: expression.input, 143 editing: true, 144 editIndex: index, 145 }); 146 } 147 148 deleteExpression(e, expression) { 149 e.stopPropagation(); 150 const { deleteExpression } = this.props; 151 deleteExpression(expression); 152 } 153 154 handleChange = e => { 155 const { target } = e; 156 if (features.autocompleteExpression) { 157 this.findAutocompleteMatches(target.value, target.selectionStart); 158 } 159 this.setState({ inputValue: target.value }); 160 }; 161 162 findAutocompleteMatches = debounce((value, selectionStart) => { 163 const { autocomplete } = this.props; 164 autocomplete(value, selectionStart); 165 }, 250); 166 167 handleKeyDown = e => { 168 if (e.key === "Escape") { 169 this.clear(); 170 } 171 }; 172 173 hideInput = () => { 174 this.props.onExpressionAdded(); 175 }; 176 177 createElement = element => { 178 return document.createElement(element); 179 }; 180 181 onBlur() { 182 this.clear(); 183 this.hideInput(); 184 } 185 186 handleExistingSubmit = async (e, expression) => { 187 e.preventDefault(); 188 e.stopPropagation(); 189 190 this.props.updateExpression(this.state.inputValue, expression); 191 }; 192 193 handleNewSubmit = async e => { 194 e.preventDefault(); 195 e.stopPropagation(); 196 197 await this.props.addExpression(this.state.inputValue); 198 this.setState({ 199 editing: false, 200 editIndex: -1, 201 inputValue: "", 202 }); 203 204 this.props.clearAutocomplete(); 205 }; 206 207 renderExpressionsNotification() { 208 const { isOriginalVariableMappingDisabled, isLoadingOriginalVariables } = 209 this.props; 210 211 if (isOriginalVariableMappingDisabled) { 212 return div( 213 { 214 className: "pane-info no-original-scopes-info", 215 "aria-role": "status", 216 }, 217 span( 218 { className: "info icon" }, 219 React.createElement(DebuggerImage, { name: "sourcemap" }) 220 ), 221 span( 222 { className: "message" }, 223 L10N.getStr("expressions.noOriginalScopes") 224 ) 225 ); 226 } 227 228 if (isLoadingOriginalVariables) { 229 return div( 230 { className: "pane-info" }, 231 span( 232 { className: "info icon" }, 233 React.createElement(DebuggerImage, { name: "loader" }) 234 ), 235 span( 236 { className: "message" }, 237 L10N.getStr("scopes.loadingOriginalScopes") 238 ) 239 ); 240 } 241 return null; 242 } 243 244 renderExpression = (expression, index) => { 245 const { 246 openLink, 247 openElementInInspector, 248 highlightDomElement, 249 unHighlightDomElement, 250 } = this.props; 251 252 const { editing, editIndex } = this.state; 253 const { input: _input, updating } = expression; 254 const isEditingExpr = editing && editIndex === index; 255 if (isEditingExpr) { 256 return this.renderExpressionEditInput(expression); 257 } 258 259 if (updating) { 260 return null; 261 } 262 263 const { expressionResultGrip, expressionResultFront } = 264 getExpressionResultGripAndFront(expression); 265 266 const root = { 267 name: expression.input, 268 path: _input, 269 contents: { 270 value: expressionResultGrip, 271 front: expressionResultFront, 272 }, 273 }; 274 275 return li( 276 { 277 className: "expression-container", 278 key: _input, 279 title: expression.input, 280 }, 281 div( 282 { 283 className: "expression-content", 284 }, 285 React.createElement(ObjectInspector, { 286 roots: [root], 287 autoExpandDepth: 0, 288 disableWrap: true, 289 openLink, 290 createElement: this.createElement, 291 onDoubleClick: (items, { depth }) => { 292 if (depth === 0) { 293 this.editExpression(expression, index); 294 } 295 }, 296 onDOMNodeClick: grip => openElementInInspector(grip), 297 onInspectIconClick: grip => openElementInInspector(grip), 298 onDOMNodeMouseOver: grip => highlightDomElement(grip), 299 onDOMNodeMouseOut: grip => unHighlightDomElement(grip), 300 shouldRenderTooltip: true, 301 mayUseCustomFormatter: true, 302 }), 303 div( 304 { 305 className: "expression-container__close-btn", 306 }, 307 React.createElement(CloseButton, { 308 handleClick: e => this.deleteExpression(e, expression), 309 tooltip: L10N.getStr("expressions.remove.tooltip"), 310 }) 311 ) 312 ) 313 ); 314 }; 315 316 renderExpressions() { 317 const { expressions, showInput } = this.props; 318 return React.createElement( 319 React.Fragment, 320 null, 321 ul( 322 { 323 className: "pane expressions-list", 324 }, 325 expressions.map(this.renderExpression) 326 ), 327 showInput && this.renderNewExpressionInput() 328 ); 329 } 330 331 renderAutoCompleteMatches() { 332 if (!features.autocompleteExpression) { 333 return null; 334 } 335 const { autocompleteMatches } = this.props; 336 if (autocompleteMatches) { 337 return datalist( 338 { 339 id: "autocomplete-matches", 340 }, 341 autocompleteMatches.map((match, index) => { 342 return option({ 343 key: index, 344 value: match, 345 }); 346 }) 347 ); 348 } 349 return datalist({ 350 id: "autocomplete-matches", 351 }); 352 } 353 354 renderNewExpressionInput() { 355 const { editing, inputValue } = this.state; 356 return form( 357 { 358 className: "expression-input-container expression-input-form", 359 onSubmit: this.handleNewSubmit, 360 }, 361 input({ 362 className: "input-expression", 363 type: "text", 364 placeholder: L10N.getStr("expressions.placeholder2"), 365 onChange: this.handleChange, 366 onBlur: this.hideInput, 367 onKeyDown: this.handleKeyDown, 368 value: !editing ? inputValue : "", 369 ref: c => (this._input = c), 370 ...(features.autocompleteExpression && { 371 list: "autocomplete-matches", 372 }), 373 }), 374 this.renderAutoCompleteMatches(), 375 input({ 376 type: "submit", 377 style: { 378 display: "none", 379 }, 380 }) 381 ); 382 } 383 384 renderExpressionEditInput(expression) { 385 const { inputValue, editing } = this.state; 386 return form( 387 { 388 key: expression.input, 389 className: "expression-input-container expression-input-form", 390 onSubmit: e => this.handleExistingSubmit(e, expression), 391 }, 392 input({ 393 className: "input-expression", 394 type: "text", 395 onChange: this.handleChange, 396 onBlur: this.clear, 397 onKeyDown: this.handleKeyDown, 398 value: editing ? inputValue : expression.input, 399 ref: c => (this._input = c), 400 ...(features.autocompleteExpression && { 401 list: "autocomplete-matches", 402 }), 403 }), 404 this.renderAutoCompleteMatches(), 405 input({ 406 type: "submit", 407 style: { 408 display: "none", 409 }, 410 }) 411 ); 412 } 413 414 render() { 415 const { expressions } = this.props; 416 417 return div( 418 { className: "pane" }, 419 this.renderExpressionsNotification(), 420 expressions.length === 0 421 ? this.renderNewExpressionInput() 422 : this.renderExpressions() 423 ); 424 } 425 } 426 427 const mapStateToProps = state => { 428 const selectedFrame = getSelectedFrame(state); 429 const selectedSource = getSelectedSource(state); 430 const isPaused = getIsCurrentThreadPaused(state); 431 const mapScopesEnabled = isMapScopesEnabled(state); 432 const expressions = getExpressions(state); 433 434 const selectedSourceIsNonPrettyPrintedOriginal = 435 selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted; 436 437 let isOriginalVariableMappingDisabled, isLoadingOriginalVariables; 438 439 if (selectedSourceIsNonPrettyPrintedOriginal) { 440 isOriginalVariableMappingDisabled = isPaused && !mapScopesEnabled; 441 isLoadingOriginalVariables = 442 isPaused && 443 mapScopesEnabled && 444 !expressions.length && 445 !getOriginalFrameScope(state, selectedFrame)?.scope; 446 } 447 448 return { 449 isOriginalVariableMappingDisabled, 450 isLoadingOriginalVariables, 451 autocompleteMatches: getAutocompleteMatchset(state), 452 expressions, 453 }; 454 }; 455 456 export default connect(mapStateToProps, { 457 autocomplete: actions.autocomplete, 458 clearAutocomplete: actions.clearAutocomplete, 459 addExpression: actions.addExpression, 460 updateExpression: actions.updateExpression, 461 deleteExpression: actions.deleteExpression, 462 openLink: actions.openLink, 463 openElementInInspector: actions.openElementInInspectorCommand, 464 highlightDomElement: actions.highlightDomElement, 465 unHighlightDomElement: actions.unHighlightDomElement, 466 })(Expressions);