tor-browser

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

SearchBox.js (6942B)


      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 /* global window */
      6 
      7 "use strict";
      8 
      9 const {
     10  createFactory,
     11  createRef,
     12  PureComponent,
     13 } = require("resource://devtools/client/shared/vendor/react.mjs");
     14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     16 
     17 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     18 const l10n = new LocalizationHelper(
     19  "devtools/client/locales/components.properties"
     20 );
     21 
     22 loader.lazyGetter(this, "SearchBoxAutocompletePopup", function () {
     23  return createFactory(
     24    require("resource://devtools/client/shared/components/SearchBoxAutocompletePopup.js")
     25  );
     26 });
     27 loader.lazyGetter(this, "MDNLink", function () {
     28  return createFactory(
     29    require("resource://devtools/client/shared/components/MdnLink.js")
     30  );
     31 });
     32 
     33 loader.lazyRequireGetter(
     34  this,
     35  "KeyShortcuts",
     36  "resource://devtools/client/shared/key-shortcuts.js"
     37 );
     38 
     39 class SearchBox extends PureComponent {
     40  static get propTypes() {
     41    return {
     42      autocompleteProvider: PropTypes.func,
     43      delay: PropTypes.number,
     44      keyShortcut: PropTypes.string,
     45      learnMoreTitle: PropTypes.string,
     46      learnMoreUrl: PropTypes.string,
     47      onBlur: PropTypes.func,
     48      onChange: PropTypes.func.isRequired,
     49      onClearButtonClick: PropTypes.func,
     50      onFocus: PropTypes.func,
     51      // Optional function that will be called on the focus keyboard shortcut, before
     52      // setting the focus to the input. If the function returns false, the input won't
     53      // get focused.
     54      onFocusKeyboardShortcut: PropTypes.func,
     55      onKeyDown: PropTypes.func,
     56      placeholder: PropTypes.string.isRequired,
     57      summary: PropTypes.string,
     58      summaryId: PropTypes.string,
     59      summaryTooltip: PropTypes.string,
     60      type: PropTypes.string,
     61      initialValue: PropTypes.string,
     62    };
     63  }
     64 
     65  constructor(props) {
     66    super(props);
     67 
     68    this.state = {
     69      value: props.initialValue || "",
     70      focused: false,
     71    };
     72 
     73    this.autocompleteRef = createRef();
     74    this.inputRef = createRef();
     75 
     76    this.onBlur = this.onBlur.bind(this);
     77    this.onChange = this.onChange.bind(this);
     78    this.onClearButtonClick = this.onClearButtonClick.bind(this);
     79    this.onFocus = this.onFocus.bind(this);
     80    this.onKeyDown = this.onKeyDown.bind(this);
     81  }
     82 
     83  componentDidMount() {
     84    if (!this.props.keyShortcut) {
     85      return;
     86    }
     87 
     88    this.shortcuts = new KeyShortcuts({
     89      window,
     90    });
     91    this.shortcuts.on(this.props.keyShortcut, event => {
     92      if (this.props.onFocusKeyboardShortcut?.(event)) {
     93        return;
     94      }
     95 
     96      event.preventDefault();
     97      this.focus();
     98    });
     99  }
    100 
    101  componentWillUnmount() {
    102    if (this.shortcuts) {
    103      this.shortcuts.destroy();
    104    }
    105 
    106    // Clean up an existing timeout.
    107    if (this.searchTimeout) {
    108      clearTimeout(this.searchTimeout);
    109    }
    110  }
    111 
    112  focus() {
    113    if (this.inputRef) {
    114      this.inputRef.current.focus();
    115    }
    116  }
    117 
    118  onChange(inputValue = "") {
    119    if (this.state.value !== inputValue) {
    120      this.setState({
    121        focused: true,
    122        value: inputValue,
    123      });
    124    }
    125 
    126    if (!this.props.delay) {
    127      this.props.onChange(inputValue);
    128      return;
    129    }
    130 
    131    // Clean up an existing timeout before creating a new one.
    132    if (this.searchTimeout) {
    133      clearTimeout(this.searchTimeout);
    134    }
    135 
    136    // Execute the search after a timeout. It makes the UX
    137    // smoother if the user is typing quickly.
    138    this.searchTimeout = setTimeout(() => {
    139      this.searchTimeout = null;
    140      this.props.onChange(this.state.value);
    141    }, this.props.delay);
    142  }
    143 
    144  onClearButtonClick() {
    145    this.onChange("");
    146 
    147    if (this.props.onClearButtonClick) {
    148      this.props.onClearButtonClick();
    149    }
    150  }
    151 
    152  onFocus() {
    153    if (this.props.onFocus) {
    154      this.props.onFocus();
    155    }
    156 
    157    this.setState({ focused: true });
    158  }
    159 
    160  onBlur() {
    161    if (this.props.onBlur) {
    162      this.props.onBlur();
    163    }
    164 
    165    this.setState({ focused: false });
    166  }
    167 
    168  onKeyDown(e) {
    169    if (this.props.onKeyDown) {
    170      this.props.onKeyDown(e);
    171    }
    172 
    173    const autocomplete = this.autocompleteRef.current;
    174    if (!autocomplete || autocomplete.state.list.length <= 0) {
    175      return;
    176    }
    177 
    178    switch (e.key) {
    179      case "ArrowDown":
    180        e.preventDefault();
    181        autocomplete.jumpBy(1);
    182        break;
    183      case "ArrowUp":
    184        e.preventDefault();
    185        autocomplete.jumpBy(-1);
    186        break;
    187      case "PageDown":
    188        e.preventDefault();
    189        autocomplete.jumpBy(5);
    190        break;
    191      case "PageUp":
    192        e.preventDefault();
    193        autocomplete.jumpBy(-5);
    194        break;
    195      case "Enter":
    196      case "Tab":
    197        e.preventDefault();
    198        autocomplete.select();
    199        break;
    200      case "Escape":
    201        e.preventDefault();
    202        this.onBlur();
    203        break;
    204      case "Home":
    205        e.preventDefault();
    206        autocomplete.jumpToTop();
    207        break;
    208      case "End":
    209        e.preventDefault();
    210        autocomplete.jumpToBottom();
    211        break;
    212    }
    213  }
    214 
    215  render() {
    216    const {
    217      autocompleteProvider,
    218      summary,
    219      summaryId,
    220      summaryTooltip,
    221      learnMoreTitle,
    222      learnMoreUrl,
    223      placeholder,
    224      type = "search",
    225    } = this.props;
    226    const { value } = this.state;
    227    const showAutocomplete =
    228      autocompleteProvider && this.state.focused && value !== "";
    229    const showLearnMoreLink = learnMoreUrl && value === "";
    230 
    231    return dom.div(
    232      { className: "devtools-searchbox" },
    233      dom.input({
    234        className: `devtools-${type}input`,
    235        onBlur: this.onBlur,
    236        onChange: e => this.onChange(e.target.value),
    237        onFocus: this.onFocus,
    238        onKeyDown: this.onKeyDown,
    239        placeholder,
    240        ref: this.inputRef,
    241        value,
    242        type: "search",
    243        "aria-describedby": (summary && summaryId) || undefined,
    244      }),
    245      showLearnMoreLink &&
    246        MDNLink({
    247          title: learnMoreTitle,
    248          url: learnMoreUrl,
    249        }),
    250      summary
    251        ? dom.span(
    252            {
    253              className: "devtools-searchinput-summary",
    254              id: summaryId,
    255              title: summaryTooltip,
    256            },
    257            summary
    258          )
    259        : null,
    260      dom.button({
    261        className: "devtools-searchinput-clear",
    262        hidden: value === "",
    263        onClick: this.onClearButtonClick,
    264        title: l10n.getStr("searchBox.clearButtonTitle"),
    265      }),
    266      showAutocomplete &&
    267        SearchBoxAutocompletePopup({
    268          autocompleteProvider,
    269          filter: value,
    270          onItemSelected: itemValue => this.onChange(itemValue),
    271          ref: this.autocompleteRef,
    272        })
    273    );
    274  }
    275 }
    276 
    277 module.exports = SearchBox;