diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/debugger/src/components/SecondaryPanes/Expressions.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/components/SecondaryPanes/Expressions.js')
-rw-r--r-- | devtools/client/debugger/src/components/SecondaryPanes/Expressions.js | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js new file mode 100644 index 0000000000..be05c7327c --- /dev/null +++ b/devtools/client/debugger/src/components/SecondaryPanes/Expressions.js @@ -0,0 +1,486 @@ +/* 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/>. */ + +import React, { Component } from "devtools/client/shared/vendor/react"; +import { + div, + input, + li, + ul, + form, + datalist, + option, + span, +} from "devtools/client/shared/vendor/react-dom-factories"; +import PropTypes from "devtools/client/shared/vendor/react-prop-types"; +import { connect } from "devtools/client/shared/vendor/react-redux"; +import { features } from "../../utils/prefs"; +import AccessibleImage from "../shared/AccessibleImage"; + +import { objectInspector } from "devtools/client/shared/components/reps/index"; + +import actions from "../../actions/index"; +import { + getExpressions, + getAutocompleteMatchset, + getSelectedSource, + isMapScopesEnabled, + getIsCurrentThreadPaused, + getSelectedFrame, + getOriginalFrameScope, + getCurrentThread, +} from "../../selectors/index"; +import { getExpressionResultGripAndFront } from "../../utils/expressions"; + +import { CloseButton } from "../shared/Button/index"; + +const { debounce } = require("resource://devtools/shared/debounce.js"); +const classnames = require("resource://devtools/client/shared/classnames.js"); + +const { ObjectInspector } = objectInspector; + +class Expressions extends Component { + constructor(props) { + super(props); + + this.state = { + editing: false, + editIndex: -1, + inputValue: "", + focused: false, + }; + } + + static get propTypes() { + return { + addExpression: PropTypes.func.isRequired, + autocomplete: PropTypes.func.isRequired, + autocompleteMatches: PropTypes.array, + clearAutocomplete: PropTypes.func.isRequired, + deleteExpression: PropTypes.func.isRequired, + expressions: PropTypes.array.isRequired, + highlightDomElement: PropTypes.func.isRequired, + onExpressionAdded: PropTypes.func.isRequired, + openElementInInspector: PropTypes.func.isRequired, + openLink: PropTypes.any.isRequired, + showInput: PropTypes.bool.isRequired, + unHighlightDomElement: PropTypes.func.isRequired, + updateExpression: PropTypes.func.isRequired, + isOriginalVariableMappingDisabled: PropTypes.bool, + isLoadingOriginalVariables: PropTypes.bool, + }; + } + + componentDidMount() { + const { showInput } = this.props; + + // Ensures that the input is focused when the "+" + // is clicked while the panel is collapsed + if (showInput && this._input) { + this._input.focus(); + } + } + + clear = () => { + this.setState(() => ({ + editing: false, + editIndex: -1, + inputValue: "", + focused: false, + })); + }; + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + if (this.state.editing) { + this.clear(); + } + + // Ensures that the add watch expression input + // is no longer visible when the new watch expression is rendered + if (this.props.expressions.length < nextProps.expressions.length) { + this.hideInput(); + } + } + + shouldComponentUpdate(nextProps, nextState) { + const { editing, inputValue, focused } = this.state; + const { + expressions, + showInput, + autocompleteMatches, + isLoadingOriginalVariables, + isOriginalVariableMappingDisabled, + } = this.props; + + return ( + autocompleteMatches !== nextProps.autocompleteMatches || + expressions !== nextProps.expressions || + isLoadingOriginalVariables !== nextProps.isLoadingOriginalVariables || + isOriginalVariableMappingDisabled !== + nextProps.isOriginalVariableMappingDisabled || + editing !== nextState.editing || + inputValue !== nextState.inputValue || + nextProps.showInput !== showInput || + focused !== nextState.focused + ); + } + + componentDidUpdate(prevProps, prevState) { + 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(); + } + } + + editExpression(expression, index) { + this.setState({ + inputValue: expression.input, + editing: true, + editIndex: index, + }); + } + + deleteExpression(e, expression) { + e.stopPropagation(); + const { deleteExpression } = this.props; + deleteExpression(expression); + } + + handleChange = e => { + const { target } = e; + if (features.autocompleteExpression) { + this.findAutocompleteMatches(target.value, target.selectionStart); + } + this.setState({ inputValue: target.value }); + }; + + findAutocompleteMatches = debounce((value, selectionStart) => { + const { autocomplete } = this.props; + autocomplete(value, selectionStart); + }, 250); + + handleKeyDown = e => { + if (e.key === "Escape") { + this.clear(); + } + }; + + hideInput = () => { + this.setState({ focused: false }); + this.props.onExpressionAdded(); + }; + + createElement = element => { + return document.createElement(element); + }; + + onFocus = () => { + this.setState({ focused: true }); + }; + + onBlur() { + this.clear(); + this.hideInput(); + } + + handleExistingSubmit = async (e, expression) => { + e.preventDefault(); + e.stopPropagation(); + + this.props.updateExpression(this.state.inputValue, expression); + }; + + handleNewSubmit = async e => { + e.preventDefault(); + e.stopPropagation(); + + await this.props.addExpression(this.state.inputValue); + this.setState({ + editing: false, + editIndex: -1, + inputValue: "", + }); + + this.props.clearAutocomplete(); + }; + + renderExpressionsNotification() { + const { isOriginalVariableMappingDisabled, isLoadingOriginalVariables } = + this.props; + + if (isOriginalVariableMappingDisabled) { + return div( + { + className: "pane-info no-original-scopes-info", + "aria-role": "status", + }, + span( + { className: "info icon" }, + React.createElement(AccessibleImage, { className: "sourcemap" }) + ), + span( + { className: "message" }, + L10N.getStr("expressions.noOriginalScopes") + ) + ); + } + + if (isLoadingOriginalVariables) { + return div( + { className: "pane-info" }, + span( + { className: "info icon" }, + React.createElement(AccessibleImage, { className: "loader" }) + ), + span( + { className: "message" }, + L10N.getStr("scopes.loadingOriginalScopes") + ) + ); + } + return null; + } + + renderExpression = (expression, index) => { + const { + openLink, + openElementInInspector, + highlightDomElement, + unHighlightDomElement, + } = this.props; + + const { editing, editIndex } = this.state; + const { input: _input, updating } = expression; + const isEditingExpr = editing && editIndex === index; + if (isEditingExpr) { + return this.renderExpressionEditInput(expression); + } + + if (updating) { + return null; + } + + const { expressionResultGrip, expressionResultFront } = + getExpressionResultGripAndFront(expression); + + const root = { + name: expression.input, + path: _input, + contents: { + value: expressionResultGrip, + front: expressionResultFront, + }, + }; + + return li( + { + className: "expression-container", + key: _input, + title: expression.input, + }, + div( + { + className: "expression-content", + }, + React.createElement(ObjectInspector, { + roots: [root], + autoExpandDepth: 0, + disableWrap: true, + openLink: openLink, + createElement: this.createElement, + onDoubleClick: (items, { depth }) => { + if (depth === 0) { + this.editExpression(expression, index); + } + }, + onDOMNodeClick: grip => openElementInInspector(grip), + onInspectIconClick: grip => openElementInInspector(grip), + onDOMNodeMouseOver: grip => highlightDomElement(grip), + onDOMNodeMouseOut: grip => unHighlightDomElement(grip), + shouldRenderTooltip: true, + mayUseCustomFormatter: true, + }), + div( + { + className: "expression-container__close-btn", + }, + React.createElement(CloseButton, { + handleClick: e => this.deleteExpression(e, expression), + tooltip: L10N.getStr("expressions.remove.tooltip"), + }) + ) + ) + ); + }; + + renderExpressions() { + const { expressions, showInput } = this.props; + return React.createElement( + React.Fragment, + null, + ul( + { + className: "pane expressions-list", + }, + expressions.map(this.renderExpression) + ), + showInput && this.renderNewExpressionInput() + ); + } + + renderAutoCompleteMatches() { + if (!features.autocompleteExpression) { + return null; + } + const { autocompleteMatches } = this.props; + if (autocompleteMatches) { + return datalist( + { + id: "autocomplete-matches", + }, + autocompleteMatches.map((match, index) => { + return option({ + key: index, + value: match, + }); + }) + ); + } + return datalist({ + id: "autocomplete-matches", + }); + } + + renderNewExpressionInput() { + const { editing, inputValue, focused } = this.state; + return form( + { + className: classnames( + "expression-input-container expression-input-form", + { focused } + ), + onSubmit: this.handleNewSubmit, + }, + input({ + className: "input-expression", + type: "text", + placeholder: L10N.getStr("expressions.placeholder"), + onChange: this.handleChange, + onBlur: this.hideInput, + onKeyDown: this.handleKeyDown, + onFocus: this.onFocus, + value: !editing ? inputValue : "", + ref: c => (this._input = c), + ...(features.autocompleteExpression && { + list: "autocomplete-matches", + }), + }), + this.renderAutoCompleteMatches(), + input({ + type: "submit", + style: { + display: "none", + }, + }) + ); + } + + renderExpressionEditInput(expression) { + const { inputValue, editing, focused } = this.state; + return form( + { + key: expression.input, + className: classnames( + "expression-input-container expression-input-form", + { + focused, + } + ), + onSubmit: e => this.handleExistingSubmit(e, expression), + }, + input({ + className: "input-expression", + type: "text", + onChange: this.handleChange, + onBlur: this.clear, + onKeyDown: this.handleKeyDown, + onFocus: this.onFocus, + value: editing ? inputValue : expression.input, + ref: c => (this._input = c), + ...(features.autocompleteExpression && { + list: "autocomplete-matches", + }), + }), + this.renderAutoCompleteMatches(), + input({ + type: "submit", + style: { + display: "none", + }, + }) + ); + } + + render() { + const { expressions } = this.props; + + return div( + { className: "pane" }, + this.renderExpressionsNotification(), + expressions.length === 0 + ? this.renderNewExpressionInput() + : this.renderExpressions() + ); + } +} + +const mapStateToProps = state => { + const selectedFrame = getSelectedFrame(state, getCurrentThread(state)); + const selectedSource = getSelectedSource(state); + const isPaused = getIsCurrentThreadPaused(state); + const mapScopesEnabled = isMapScopesEnabled(state); + const expressions = getExpressions(state); + + const selectedSourceIsNonPrettyPrintedOriginal = + selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted; + + let isOriginalVariableMappingDisabled, isLoadingOriginalVariables; + + if (selectedSourceIsNonPrettyPrintedOriginal) { + isOriginalVariableMappingDisabled = isPaused && !mapScopesEnabled; + isLoadingOriginalVariables = + isPaused && + mapScopesEnabled && + !expressions.length && + !getOriginalFrameScope(state, selectedFrame)?.scope; + } + + return { + isOriginalVariableMappingDisabled, + isLoadingOriginalVariables, + autocompleteMatches: getAutocompleteMatchset(state), + expressions, + }; +}; + +export default connect(mapStateToProps, { + autocomplete: actions.autocomplete, + clearAutocomplete: actions.clearAutocomplete, + addExpression: actions.addExpression, + updateExpression: actions.updateExpression, + deleteExpression: actions.deleteExpression, + openLink: actions.openLink, + openElementInInspector: actions.openElementInInspectorCommand, + highlightDomElement: actions.highlightDomElement, + unHighlightDomElement: actions.unHighlightDomElement, +})(Expressions); |