277 lines
6.8 KiB
JavaScript
277 lines
6.8 KiB
JavaScript
/* 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/. */
|
|
|
|
/* global window */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
createFactory,
|
|
createRef,
|
|
PureComponent,
|
|
} = require("resource://devtools/client/shared/vendor/react.js");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
|
|
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
|
|
const l10n = new LocalizationHelper(
|
|
"devtools/client/locales/components.properties"
|
|
);
|
|
|
|
loader.lazyGetter(this, "SearchBoxAutocompletePopup", function () {
|
|
return createFactory(
|
|
require("resource://devtools/client/shared/components/SearchBoxAutocompletePopup.js")
|
|
);
|
|
});
|
|
loader.lazyGetter(this, "MDNLink", function () {
|
|
return createFactory(
|
|
require("resource://devtools/client/shared/components/MdnLink.js")
|
|
);
|
|
});
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"KeyShortcuts",
|
|
"resource://devtools/client/shared/key-shortcuts.js"
|
|
);
|
|
|
|
class SearchBox extends PureComponent {
|
|
static get propTypes() {
|
|
return {
|
|
autocompleteProvider: PropTypes.func,
|
|
delay: PropTypes.number,
|
|
keyShortcut: PropTypes.string,
|
|
learnMoreTitle: PropTypes.string,
|
|
learnMoreUrl: PropTypes.string,
|
|
onBlur: PropTypes.func,
|
|
onChange: PropTypes.func.isRequired,
|
|
onClearButtonClick: PropTypes.func,
|
|
onFocus: PropTypes.func,
|
|
// Optional function that will be called on the focus keyboard shortcut, before
|
|
// setting the focus to the input. If the function returns false, the input won't
|
|
// get focused.
|
|
onFocusKeyboardShortcut: PropTypes.func,
|
|
onKeyDown: PropTypes.func,
|
|
placeholder: PropTypes.string.isRequired,
|
|
summary: PropTypes.string,
|
|
summaryId: PropTypes.string,
|
|
summaryTooltip: PropTypes.string,
|
|
type: PropTypes.string,
|
|
value: PropTypes.string,
|
|
};
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
value: props.value || "",
|
|
focused: false,
|
|
};
|
|
|
|
this.autocompleteRef = createRef();
|
|
this.inputRef = createRef();
|
|
|
|
this.onBlur = this.onBlur.bind(this);
|
|
this.onChange = this.onChange.bind(this);
|
|
this.onClearButtonClick = this.onClearButtonClick.bind(this);
|
|
this.onFocus = this.onFocus.bind(this);
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (!this.props.keyShortcut) {
|
|
return;
|
|
}
|
|
|
|
this.shortcuts = new KeyShortcuts({
|
|
window,
|
|
});
|
|
this.shortcuts.on(this.props.keyShortcut, event => {
|
|
if (this.props.onFocusKeyboardShortcut?.(event)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
this.focus();
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.shortcuts) {
|
|
this.shortcuts.destroy();
|
|
}
|
|
|
|
// Clean up an existing timeout.
|
|
if (this.searchTimeout) {
|
|
clearTimeout(this.searchTimeout);
|
|
}
|
|
}
|
|
|
|
focus() {
|
|
if (this.inputRef) {
|
|
this.inputRef.current.focus();
|
|
}
|
|
}
|
|
|
|
onChange(inputValue = "") {
|
|
if (this.state.value !== inputValue) {
|
|
this.setState({
|
|
focused: true,
|
|
value: inputValue,
|
|
});
|
|
}
|
|
|
|
if (!this.props.delay) {
|
|
this.props.onChange(inputValue);
|
|
return;
|
|
}
|
|
|
|
// Clean up an existing timeout before creating a new one.
|
|
if (this.searchTimeout) {
|
|
clearTimeout(this.searchTimeout);
|
|
}
|
|
|
|
// Execute the search after a timeout. It makes the UX
|
|
// smoother if the user is typing quickly.
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.searchTimeout = null;
|
|
this.props.onChange(this.state.value);
|
|
}, this.props.delay);
|
|
}
|
|
|
|
onClearButtonClick() {
|
|
this.onChange("");
|
|
|
|
if (this.props.onClearButtonClick) {
|
|
this.props.onClearButtonClick();
|
|
}
|
|
}
|
|
|
|
onFocus() {
|
|
if (this.props.onFocus) {
|
|
this.props.onFocus();
|
|
}
|
|
|
|
this.setState({ focused: true });
|
|
}
|
|
|
|
onBlur() {
|
|
if (this.props.onBlur) {
|
|
this.props.onBlur();
|
|
}
|
|
|
|
this.setState({ focused: false });
|
|
}
|
|
|
|
onKeyDown(e) {
|
|
if (this.props.onKeyDown) {
|
|
this.props.onKeyDown(e);
|
|
}
|
|
|
|
const autocomplete = this.autocompleteRef.current;
|
|
if (!autocomplete || autocomplete.state.list.length <= 0) {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
autocomplete.jumpBy(1);
|
|
break;
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
autocomplete.jumpBy(-1);
|
|
break;
|
|
case "PageDown":
|
|
e.preventDefault();
|
|
autocomplete.jumpBy(5);
|
|
break;
|
|
case "PageUp":
|
|
e.preventDefault();
|
|
autocomplete.jumpBy(-5);
|
|
break;
|
|
case "Enter":
|
|
case "Tab":
|
|
e.preventDefault();
|
|
autocomplete.select();
|
|
break;
|
|
case "Escape":
|
|
e.preventDefault();
|
|
this.onBlur();
|
|
break;
|
|
case "Home":
|
|
e.preventDefault();
|
|
autocomplete.jumpToTop();
|
|
break;
|
|
case "End":
|
|
e.preventDefault();
|
|
autocomplete.jumpToBottom();
|
|
break;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
autocompleteProvider,
|
|
summary,
|
|
summaryId,
|
|
summaryTooltip,
|
|
learnMoreTitle,
|
|
learnMoreUrl,
|
|
placeholder,
|
|
type = "search",
|
|
} = this.props;
|
|
const { value } = this.state;
|
|
const showAutocomplete =
|
|
autocompleteProvider && this.state.focused && value !== "";
|
|
const showLearnMoreLink = learnMoreUrl && value === "";
|
|
|
|
return dom.div(
|
|
{ className: "devtools-searchbox" },
|
|
dom.input({
|
|
className: `devtools-${type}input`,
|
|
onBlur: this.onBlur,
|
|
onChange: e => this.onChange(e.target.value),
|
|
onFocus: this.onFocus,
|
|
onKeyDown: this.onKeyDown,
|
|
placeholder,
|
|
ref: this.inputRef,
|
|
value,
|
|
type: "search",
|
|
"aria-describedby": (summary && summaryId) || undefined,
|
|
}),
|
|
showLearnMoreLink &&
|
|
MDNLink({
|
|
title: learnMoreTitle,
|
|
url: learnMoreUrl,
|
|
}),
|
|
summary
|
|
? dom.span(
|
|
{
|
|
className: "devtools-searchinput-summary",
|
|
id: summaryId,
|
|
title: summaryTooltip,
|
|
},
|
|
summary
|
|
)
|
|
: null,
|
|
dom.button({
|
|
className: "devtools-searchinput-clear",
|
|
hidden: value === "",
|
|
onClick: this.onClearButtonClick,
|
|
title: l10n.getStr("searchBox.clearButtonTitle"),
|
|
}),
|
|
showAutocomplete &&
|
|
SearchBoxAutocompletePopup({
|
|
autocompleteProvider,
|
|
filter: value,
|
|
onItemSelected: itemValue => this.onChange(itemValue),
|
|
ref: this.autocompleteRef,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = SearchBox;
|