зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1364093 - After a space and character, autocomplete should be shown for all flags. r=ntim, jdescottes.
MozReview-Commit-ID: JTPvHLBzIB0
This commit is contained in:
Родитель
d0467bd4b4
Коммит
58a62d7ade
|
@ -12,13 +12,14 @@ const {
|
|||
} = require("devtools/client/shared/vendor/react");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const Actions = require("../actions/index");
|
||||
const { FILTER_SEARCH_DELAY, FILTER_FLAGS } = require("../constants");
|
||||
const { FILTER_SEARCH_DELAY } = require("../constants");
|
||||
const {
|
||||
getDisplayedRequestsSummary,
|
||||
getRequestFilterTypes,
|
||||
isNetworkDetailsToggleButtonDisabled,
|
||||
} = require("../selectors/index");
|
||||
|
||||
const { autocompleteProvider } = require("../utils/filter-text-utils");
|
||||
const { L10N } = require("../utils/l10n");
|
||||
|
||||
// Components
|
||||
|
@ -92,11 +93,6 @@ const Toolbar = createClass({
|
|||
);
|
||||
});
|
||||
|
||||
// Setup autocomplete list
|
||||
let negativeAutocompleteList = FILTER_FLAGS.map((item) => `-${item}`);
|
||||
let autocompleteList = [...FILTER_FLAGS, ...negativeAutocompleteList]
|
||||
.map((item) => `${item}:`);
|
||||
|
||||
return (
|
||||
span({ className: "devtools-toolbar devtools-toolbar-container" },
|
||||
span({ className: "devtools-toolbar-group" },
|
||||
|
@ -114,7 +110,7 @@ const Toolbar = createClass({
|
|||
placeholder: SEARCH_PLACE_HOLDER,
|
||||
type: "filter",
|
||||
onChange: setRequestFilterText,
|
||||
autocompleteList,
|
||||
autocompleteProvider,
|
||||
}),
|
||||
button({
|
||||
className: toggleButtonClassName.join(" "),
|
||||
|
|
|
@ -242,6 +242,50 @@ function isFreetextMatch(item, text) {
|
|||
return match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an autocomplete list for the search-box for network monitor
|
||||
*
|
||||
* It expects an entire string of the searchbox ie "is:cached pr".
|
||||
* The string is then tokenized into "is:cached" and "pr"
|
||||
*
|
||||
* @param {string} filter - The entire search string of the search box
|
||||
* @return {Array} - The output is an array of objects as below
|
||||
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
|
||||
* `value` is used to update the search-box input box for given item
|
||||
* `displayValue` is used to render the autocomplete list
|
||||
*/
|
||||
function autocompleteProvider(filter) {
|
||||
if (!filter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let negativeAutocompleteList = FILTER_FLAGS.map((item) => `-${item}`);
|
||||
let baseList = [...FILTER_FLAGS, ...negativeAutocompleteList]
|
||||
.map((item) => `${item}:`);
|
||||
|
||||
// The last token is used to filter the base autocomplete list
|
||||
let tokens = filter.split(/\s+/g);
|
||||
let lastToken = tokens[tokens.length - 1];
|
||||
let previousTokens = tokens.slice(0, tokens.length - 1);
|
||||
|
||||
// Autocomplete list is not generated for empty lastToken
|
||||
if (!lastToken) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return baseList
|
||||
.filter((item) => {
|
||||
return item.toLowerCase().startsWith(lastToken.toLowerCase())
|
||||
&& item.toLowerCase() !== lastToken.toLowerCase();
|
||||
})
|
||||
.sort()
|
||||
.map(item => ({
|
||||
value: [...previousTokens, item].join(" "),
|
||||
displayValue: item,
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isFreetextMatch,
|
||||
autocompleteProvider,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,15 @@ module.exports = createClass({
|
|||
displayName: "AutocompletePopup",
|
||||
|
||||
propTypes: {
|
||||
list: PropTypes.array.isRequired,
|
||||
/**
|
||||
* autocompleteProvider takes search-box's entire input text as `filter` argument
|
||||
* ie. "is:cached pr"
|
||||
* returned value is array of objects like below
|
||||
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
|
||||
* `value` is used to update the search-box input box for given item
|
||||
* `displayValue` is used to render the autocomplete list
|
||||
*/
|
||||
autocompleteProvider: PropTypes.func.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
onItemSelected: PropTypes.func.isRequired,
|
||||
},
|
||||
|
@ -32,14 +40,11 @@ module.exports = createClass({
|
|||
}
|
||||
},
|
||||
|
||||
computeState({ filter, list }) {
|
||||
let filteredList = list.filter((item) => {
|
||||
return item.toLowerCase().startsWith(filter.toLowerCase())
|
||||
&& item.toLowerCase() !== filter.toLowerCase();
|
||||
}).sort();
|
||||
let selectedIndex = filteredList.length == 1 ? 0 : -1;
|
||||
computeState({ autocompleteProvider, filter }) {
|
||||
let list = autocompleteProvider(filter);
|
||||
let selectedIndex = list.length == 1 ? 0 : -1;
|
||||
|
||||
return { filteredList, selectedIndex };
|
||||
return { list, selectedIndex };
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -55,8 +60,7 @@ module.exports = createClass({
|
|||
* This method is public.
|
||||
*/
|
||||
jumpToBottom() {
|
||||
let selectedIndex = this.state.filteredList.length - 1;
|
||||
this.setState({ selectedIndex });
|
||||
this.setState({ selectedIndex: this.state.list.length - 1 });
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -67,14 +71,14 @@ module.exports = createClass({
|
|||
* @param {number} increment - No. of hops in the direction
|
||||
*/
|
||||
jumpBy(increment = 1) {
|
||||
let { filteredList, selectedIndex } = this.state;
|
||||
let { list, selectedIndex } = this.state;
|
||||
let nextIndex = selectedIndex + increment;
|
||||
if (increment > 0) {
|
||||
// Positive cycling
|
||||
nextIndex = nextIndex > filteredList.length - 1 ? 0 : nextIndex;
|
||||
nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex;
|
||||
} else if (increment < 0) {
|
||||
// Inverse cycling
|
||||
nextIndex = nextIndex < 0 ? filteredList.length - 1 : nextIndex;
|
||||
nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
|
||||
}
|
||||
this.setState({selectedIndex: nextIndex});
|
||||
},
|
||||
|
@ -85,7 +89,7 @@ module.exports = createClass({
|
|||
*/
|
||||
select() {
|
||||
if (this.refs.selected) {
|
||||
this.props.onItemSelected(this.refs.selected.textContent);
|
||||
this.props.onItemSelected(this.refs.selected.dataset.value);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -95,13 +99,13 @@ module.exports = createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
let { filteredList } = this.state;
|
||||
let { list } = this.state;
|
||||
|
||||
return filteredList.length > 0 && dom.div(
|
||||
return list.length > 0 && dom.div(
|
||||
{ className: "devtools-autocomplete-popup devtools-monospace" },
|
||||
dom.ul(
|
||||
{ className: "devtools-autocomplete-listbox" },
|
||||
filteredList.map((item, i) => {
|
||||
list.map((item, i) => {
|
||||
let isSelected = this.state.selectedIndex == i;
|
||||
let itemClassList = ["autocomplete-item"];
|
||||
|
||||
|
@ -111,10 +115,11 @@ module.exports = createClass({
|
|||
return dom.li({
|
||||
key: i,
|
||||
"data-index": i,
|
||||
"data-value": item.value,
|
||||
className: itemClassList.join(" "),
|
||||
ref: isSelected ? "selected" : null,
|
||||
onMouseDown: this.onMouseDown,
|
||||
}, item);
|
||||
}, item.displayValue);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -22,13 +22,7 @@ module.exports = createClass({
|
|||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
autocompleteList: PropTypes.array,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
autocompleteList: [],
|
||||
};
|
||||
autocompleteProvider: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -66,6 +60,7 @@ module.exports = createClass({
|
|||
onChange() {
|
||||
if (this.state.value !== this.refs.input.value) {
|
||||
this.setState({
|
||||
focused: true,
|
||||
value: this.refs.input.value,
|
||||
});
|
||||
}
|
||||
|
@ -102,10 +97,8 @@ module.exports = createClass({
|
|||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
let { autocompleteList } = this.props;
|
||||
let { autocomplete } = this.refs;
|
||||
|
||||
if (autocompleteList.length == 0) {
|
||||
if (!autocomplete || autocomplete.state.list.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -144,13 +137,12 @@ module.exports = createClass({
|
|||
let {
|
||||
type = "search",
|
||||
placeholder,
|
||||
autocompleteList
|
||||
autocompleteProvider,
|
||||
} = this.props;
|
||||
let { value } = this.state;
|
||||
let divClassList = ["devtools-searchbox", "has-clear-btn"];
|
||||
let inputClassList = [`devtools-${type}input`];
|
||||
let showAutocomplete =
|
||||
autocompleteList.length > 0 && this.state.focused && value !== "";
|
||||
let showAutocomplete = autocompleteProvider && this.state.focused && value !== "";
|
||||
|
||||
if (value !== "") {
|
||||
inputClassList.push("filled");
|
||||
|
@ -173,7 +165,7 @@ module.exports = createClass({
|
|||
onClick: this.onClearButtonClick
|
||||
}),
|
||||
showAutocomplete && AutocompletePopup({
|
||||
list: autocompleteList,
|
||||
autocompleteProvider,
|
||||
filter: value,
|
||||
ref: "autocomplete",
|
||||
onItemSelected: (itemValue) => {
|
||||
|
|
|
@ -43,20 +43,44 @@ window.onload = async function () {
|
|||
);
|
||||
const { component, $ } = await createComponentTest(SearchBox, {
|
||||
type: "search",
|
||||
autocompleteList: [
|
||||
"foo",
|
||||
"BAR",
|
||||
"baZ",
|
||||
"abc",
|
||||
"pqr",
|
||||
"xyz",
|
||||
"ABC",
|
||||
"a1",
|
||||
"a2",
|
||||
"a3",
|
||||
"a4",
|
||||
"a5",
|
||||
],
|
||||
autocompleteProvider: (filter) => {
|
||||
let baseList = [
|
||||
"foo",
|
||||
"BAR",
|
||||
"baZ",
|
||||
"abc",
|
||||
"pqr",
|
||||
"xyz",
|
||||
"ABC",
|
||||
"a1",
|
||||
"a2",
|
||||
"a3",
|
||||
"a4",
|
||||
"a5",
|
||||
];
|
||||
if (!filter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tokens = filter.split(/\s+/g);
|
||||
let lastToken = tokens[tokens.length - 1];
|
||||
let previousTokens = tokens.slice(0, tokens.length - 1);
|
||||
|
||||
if (!lastToken) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return baseList
|
||||
.filter((item) => {
|
||||
return item.toLowerCase().startsWith(lastToken.toLowerCase())
|
||||
&& item.toLowerCase() !== lastToken.toLowerCase();
|
||||
})
|
||||
.sort()
|
||||
.map(item => ({
|
||||
value: [...previousTokens, item].join(" "),
|
||||
displayValue: item,
|
||||
}));
|
||||
},
|
||||
onChange: () => null,
|
||||
});
|
||||
const { refs } = component;
|
||||
|
@ -150,6 +174,8 @@ window.onload = async function () {
|
|||
ok(!$(".devtools-autocomplete-popup"), "Enter/Return hides the popup");
|
||||
|
||||
// Escape should remove the autocomplete component
|
||||
synthesizeKey("VK_BACK_SPACE", {});
|
||||
await forceRender(component);
|
||||
synthesizeKey("VK_ESCAPE", {});
|
||||
await forceRender(component);
|
||||
ok(!$(".devtools-autocomplete-popup"),
|
||||
|
@ -182,10 +208,25 @@ window.onload = async function () {
|
|||
ok(!$(".devtools-autocomplete-popup"), "Mouse click on item hides the popup");
|
||||
}
|
||||
|
||||
async function testTokenizedAutocomplete() {
|
||||
// Test for string "pqr ab" which should show list of ABC, abc
|
||||
sendString(" ab");
|
||||
await forceRender(component);
|
||||
compareAutocompleteList($(".devtools-autocomplete-listbox"), ["ABC", "abc"]);
|
||||
|
||||
// Select the first element, value now should be "pqr ABC"
|
||||
synthesizeMouseAtCenter(
|
||||
$(".devtools-autocomplete-listbox .autocomplete-item:nth-child(1)"),
|
||||
{}, window
|
||||
);
|
||||
is(component.state.value, "pqr ABC", "Post Tokenization value selection");
|
||||
}
|
||||
|
||||
add_task(async function () {
|
||||
await testSearchBoxWithAutocomplete();
|
||||
await testKeyEventsWithAutocomplete();
|
||||
await testMouseEventsWithAutocomplete();
|
||||
await testTokenizedAutocomplete();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
Загрузка…
Ссылка в новой задаче