tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);