tor-browser

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

SearchBoxAutocompletePopup.js (4424B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  Component,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 
     13 class SearchBoxAutocompletePopup extends Component {
     14  static get propTypes() {
     15    return {
     16      /**
     17       * autocompleteProvider takes search-box's entire input text as `filter` argument
     18       * ie. "is:cached pr"
     19       * returned value is array of objects like below
     20       * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
     21       * `value` is used to update the search-box input box for given item
     22       * `displayValue` is used to render the autocomplete list
     23       */
     24      autocompleteProvider: PropTypes.func.isRequired,
     25      filter: PropTypes.string.isRequired,
     26      onItemSelected: PropTypes.func.isRequired,
     27    };
     28  }
     29 
     30  static computeState({ autocompleteProvider, filter }) {
     31    const list = autocompleteProvider(filter);
     32    const selectedIndex = list.length ? 0 : -1;
     33 
     34    return { list, selectedIndex, prevFilter: filter };
     35  }
     36 
     37  static getDerivedStateFromProps(props, state) {
     38    if (props.filter !== state.prevFilter) {
     39      return SearchBoxAutocompletePopup.computeState(props);
     40    }
     41    return null;
     42  }
     43 
     44  constructor(props, context) {
     45    super(props, context);
     46    this.state = SearchBoxAutocompletePopup.computeState(props);
     47    this.jumpToTop = this.jumpToTop.bind(this);
     48    this.jumpToBottom = this.jumpToBottom.bind(this);
     49    this.jumpBy = this.jumpBy.bind(this);
     50    this.select = this.select.bind(this);
     51    this.onMouseDown = this.onMouseDown.bind(this);
     52  }
     53 
     54  componentDidUpdate() {
     55    if (this.refs.selected) {
     56      this.refs.selected.scrollIntoView(false);
     57    }
     58  }
     59 
     60  /**
     61   * Use this method to select the top-most item
     62   * This method is public, called outside of the autocomplete-popup component.
     63   */
     64  jumpToTop() {
     65    this.setState({ selectedIndex: 0 });
     66  }
     67 
     68  /**
     69   * Use this method to select the bottom-most item
     70   * This method is public.
     71   */
     72  jumpToBottom() {
     73    this.setState({ selectedIndex: this.state.list.length - 1 });
     74  }
     75 
     76  /**
     77   * Increment the selected index with the provided increment value. Will cycle to the
     78   * beginning/end of the list if the index exceeds the list boundaries.
     79   * This method is public.
     80   *
     81   * @param {number} increment - No. of hops in the direction
     82   */
     83  jumpBy(increment = 1) {
     84    const { list, selectedIndex } = this.state;
     85    let nextIndex = selectedIndex + increment;
     86    if (increment > 0) {
     87      // Positive cycling
     88      nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex;
     89    } else if (increment < 0) {
     90      // Inverse cycling
     91      nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
     92    }
     93    this.setState({ selectedIndex: nextIndex });
     94  }
     95 
     96  /**
     97   * Submit the currently selected item to the onItemSelected callback
     98   * This method is public.
     99   */
    100  select() {
    101    if (this.refs.selected) {
    102      this.props.onItemSelected(this.refs.selected.dataset.value);
    103    }
    104  }
    105 
    106  onMouseDown(e) {
    107    e.preventDefault();
    108    this.setState(
    109      { selectedIndex: Number(e.target.dataset.index) },
    110      this.select
    111    );
    112  }
    113 
    114  render() {
    115    const { list } = this.state;
    116 
    117    return (
    118      !!list.length &&
    119      dom.div(
    120        { className: "devtools-autocomplete-popup devtools-monospace" },
    121        dom.ul(
    122          { className: "devtools-autocomplete-listbox" },
    123          list.map((item, i) => {
    124            const isSelected = this.state.selectedIndex == i;
    125            const itemClassList = ["autocomplete-item"];
    126 
    127            if (isSelected) {
    128              itemClassList.push("autocomplete-selected");
    129            }
    130            return dom.li(
    131              {
    132                key: i,
    133                "data-index": i,
    134                "data-value": item.value,
    135                className: itemClassList.join(" "),
    136                ref: isSelected ? "selected" : null,
    137                onMouseDown: this.onMouseDown,
    138              },
    139              item.displayValue
    140            );
    141          })
    142        )
    143      )
    144    );
    145  }
    146 }
    147 
    148 module.exports = SearchBoxAutocompletePopup;