tor-browser

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

SearchInput.js (8970B)


      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  button,
      8  div,
      9  label,
     10  input,
     11  span,
     12 } from "devtools/client/shared/vendor/react-dom-factories";
     13 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
     14 import { connect } from "devtools/client/shared/vendor/react-redux";
     15 import { CloseButton } from "./Button/index";
     16 
     17 import DebuggerImage from "./DebuggerImage";
     18 import actions from "../../actions/index";
     19 import { getSearchOptions } from "../../selectors/index";
     20 
     21 const classnames = require("resource://devtools/client/shared/classnames.js");
     22 const SearchModifiers = require("resource://devtools/client/shared/components/SearchModifiers.js");
     23 
     24 const arrowBtn = (onClick, type, className, tooltip) => {
     25  const props = {
     26    className,
     27    key: type,
     28    onClick,
     29    title: tooltip,
     30    type,
     31  };
     32  return button(
     33    props,
     34    React.createElement(DebuggerImage, {
     35      name: type,
     36    })
     37  );
     38 };
     39 
     40 export class SearchInput extends Component {
     41  static defaultProps = {
     42    expanded: false,
     43    hasPrefix: false,
     44    selectedItemId: "",
     45    size: "",
     46    showClose: true,
     47  };
     48 
     49  constructor(props) {
     50    super(props);
     51    this.state = {
     52      history: [],
     53      excludePatterns: this.props.showSearchModifiers
     54        ? props.searchOptions.excludePatterns
     55        : null,
     56    };
     57  }
     58 
     59  static get propTypes() {
     60    return {
     61      count: PropTypes.number.isRequired,
     62      expanded: PropTypes.bool.isRequired,
     63      handleClose: PropTypes.func,
     64      handleNext: PropTypes.func,
     65      handlePrev: PropTypes.func,
     66      hasPrefix: PropTypes.bool.isRequired,
     67      isLoading: PropTypes.bool.isRequired,
     68      onBlur: PropTypes.func,
     69      onChange: PropTypes.func,
     70      onFocus: PropTypes.func,
     71      onHistoryScroll: PropTypes.func,
     72      onKeyDown: PropTypes.func,
     73      onKeyUp: PropTypes.func,
     74      placeholder: PropTypes.string,
     75      query: PropTypes.string,
     76      selectedItemId: PropTypes.string,
     77      shouldFocus: PropTypes.bool,
     78      showClose: PropTypes.bool.isRequired,
     79      showExcludePatterns: PropTypes.bool.isRequired,
     80      excludePatternsLabel: PropTypes.string,
     81      excludePatternsPlaceholder: PropTypes.string,
     82      showErrorEmoji: PropTypes.bool.isRequired,
     83      size: PropTypes.string,
     84      disabled: PropTypes.bool,
     85      summaryMsg: PropTypes.string,
     86      searchKey: PropTypes.string.isRequired,
     87      searchOptions: PropTypes.object,
     88      setSearchOptions: PropTypes.func,
     89      showSearchModifiers: PropTypes.bool.isRequired,
     90      onToggleSearchModifier: PropTypes.func,
     91    };
     92  }
     93 
     94  componentDidMount() {
     95    this.setFocus();
     96  }
     97 
     98  componentDidUpdate(prevProps) {
     99    if (this.props.shouldFocus && !prevProps.shouldFocus) {
    100      this.setFocus();
    101    }
    102  }
    103 
    104  setFocus() {
    105    if (this.$input) {
    106      const _input = this.$input;
    107      _input.focus();
    108 
    109      if (!_input.value) {
    110        return;
    111      }
    112 
    113      // omit prefix @:# from being selected
    114      const selectStartPos = this.props.hasPrefix ? 1 : 0;
    115      _input.setSelectionRange(selectStartPos, _input.value.length + 1);
    116    }
    117  }
    118 
    119  renderArrowButtons() {
    120    const { handleNext, handlePrev } = this.props;
    121 
    122    return [
    123      arrowBtn(
    124        handlePrev,
    125        "arrow-up",
    126        classnames("nav-btn", "prev"),
    127        L10N.getFormatStr("editor.searchResults.prevResult")
    128      ),
    129      arrowBtn(
    130        handleNext,
    131        "arrow-down",
    132        classnames("nav-btn", "next"),
    133        L10N.getFormatStr("editor.searchResults.nextResult")
    134      ),
    135    ];
    136  }
    137 
    138  onFocus = e => {
    139    const { onFocus } = this.props;
    140 
    141    if (onFocus) {
    142      onFocus(e);
    143    }
    144  };
    145 
    146  onBlur = e => {
    147    const { onBlur } = this.props;
    148 
    149    if (onBlur) {
    150      onBlur(e);
    151    }
    152  };
    153 
    154  onKeyDown = e => {
    155    const { onHistoryScroll, onKeyDown } = this.props;
    156    if (!onHistoryScroll) {
    157      onKeyDown(e);
    158      return;
    159    }
    160 
    161    const inputValue = e.target.value;
    162    const { history } = this.state;
    163    const currentHistoryIndex = history.indexOf(inputValue);
    164 
    165    if (e.key === "Enter") {
    166      this.saveEnteredTerm(inputValue);
    167      onKeyDown(e);
    168      return;
    169    }
    170 
    171    if (e.key === "ArrowUp") {
    172      const previous =
    173        currentHistoryIndex > -1 ? currentHistoryIndex - 1 : history.length - 1;
    174      const previousInHistory = history[previous];
    175      if (previousInHistory) {
    176        e.preventDefault();
    177        onHistoryScroll(previousInHistory);
    178      }
    179      return;
    180    }
    181 
    182    if (e.key === "ArrowDown") {
    183      const next = currentHistoryIndex + 1;
    184      const nextInHistory = history[next];
    185      if (nextInHistory) {
    186        onHistoryScroll(nextInHistory);
    187      }
    188    }
    189  };
    190 
    191  onExcludeKeyDown = e => {
    192    if (e.key === "Enter") {
    193      this.props.setSearchOptions(this.props.searchKey, {
    194        excludePatterns: this.state.excludePatterns,
    195      });
    196      this.props.onKeyDown(e);
    197    }
    198  };
    199 
    200  saveEnteredTerm(query) {
    201    const { history } = this.state;
    202    const previousIndex = history.indexOf(query);
    203    if (previousIndex !== -1) {
    204      history.splice(previousIndex, 1);
    205    }
    206    history.push(query);
    207    this.setState({ history });
    208  }
    209 
    210  renderSummaryMsg() {
    211    const { summaryMsg } = this.props;
    212 
    213    if (!summaryMsg) {
    214      return null;
    215    }
    216    return div(
    217      {
    218        className: "search-field-summary",
    219      },
    220      summaryMsg
    221    );
    222  }
    223 
    224  renderSpinner() {
    225    const { isLoading } = this.props;
    226    if (!isLoading) {
    227      return null;
    228    }
    229    return React.createElement(DebuggerImage, {
    230      name: "loader",
    231      className: "spin",
    232    });
    233  }
    234 
    235  renderNav() {
    236    const { count, handleNext, handlePrev } = this.props;
    237    if ((!handleNext && !handlePrev) || !count || count == 1) {
    238      return null;
    239    }
    240    return div(
    241      {
    242        className: "search-nav-buttons",
    243      },
    244      this.renderArrowButtons()
    245    );
    246  }
    247 
    248  renderSearchModifiers() {
    249    if (!this.props.showSearchModifiers) {
    250      return null;
    251    }
    252    return React.createElement(SearchModifiers, {
    253      modifiers: this.props.searchOptions,
    254      onToggleSearchModifier: updatedOptions => {
    255        this.props.setSearchOptions(this.props.searchKey, updatedOptions);
    256        this.props.onToggleSearchModifier();
    257      },
    258    });
    259  }
    260 
    261  renderExcludePatterns() {
    262    if (!this.props.showExcludePatterns) {
    263      return null;
    264    }
    265    return div(
    266      {
    267        className: classnames("exclude-patterns-field", this.props.size),
    268      },
    269      label(null, this.props.excludePatternsLabel),
    270      input({
    271        placeholder: this.props.excludePatternsPlaceholder,
    272        value: this.state.excludePatterns,
    273        onKeyDown: this.onExcludeKeyDown,
    274        onChange: e =>
    275          this.setState({
    276            excludePatterns: e.target.value,
    277          }),
    278      })
    279    );
    280  }
    281 
    282  renderClose() {
    283    if (!this.props.showClose) {
    284      return null;
    285    }
    286    return React.createElement(
    287      React.Fragment,
    288      null,
    289      span({
    290        className: "pipe-divider",
    291      }),
    292      React.createElement(CloseButton, {
    293        handleClick: this.props.handleClose,
    294        buttonClass: this.props.size,
    295      })
    296    );
    297  }
    298 
    299  render() {
    300    const {
    301      expanded,
    302      onChange,
    303      onKeyUp,
    304      placeholder,
    305      query,
    306      selectedItemId,
    307      showErrorEmoji,
    308      size,
    309      disabled,
    310    } = this.props;
    311 
    312    const inputProps = {
    313      className: classnames({
    314        empty: showErrorEmoji,
    315      }),
    316      disabled,
    317      onChange,
    318      onKeyDown: e => this.onKeyDown(e),
    319      onKeyUp,
    320      onFocus: e => this.onFocus(e),
    321      onBlur: e => this.onBlur(e),
    322      "aria-autocomplete": "list",
    323      "aria-controls": "result-list",
    324      "aria-activedescendant":
    325        expanded && selectedItemId ? `${selectedItemId}-title` : "",
    326      placeholder,
    327      value: query,
    328      spellCheck: false,
    329      ref: c => (this.$input = c),
    330    };
    331    return div(
    332      {
    333        className: "search-outline",
    334      },
    335      div(
    336        {
    337          className: classnames("search-field", size),
    338          role: "combobox",
    339          "aria-haspopup": "listbox",
    340          "aria-owns": "result-list",
    341          "aria-expanded": expanded,
    342        },
    343        React.createElement(DebuggerImage, {
    344          name: "search",
    345        }),
    346        input(inputProps),
    347        this.renderSpinner(),
    348        this.renderSummaryMsg(),
    349        this.renderNav(),
    350        div(
    351          {
    352            className: "search-buttons-bar",
    353          },
    354          this.renderSearchModifiers(),
    355          this.renderClose()
    356        )
    357      ),
    358      this.renderExcludePatterns()
    359    );
    360  }
    361 }
    362 const mapStateToProps = (state, props) => ({
    363  searchOptions: getSearchOptions(state, props.searchKey),
    364 });
    365 
    366 export default connect(mapStateToProps, {
    367  setSearchOptions: actions.setSearchOptions,
    368 })(SearchInput);