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:
Ruturaj K. Vartak 2017-06-07 21:51:00 +02:00
Родитель d0467bd4b4
Коммит 58a62d7ade
5 изменённых файлов: 131 добавлений и 53 удалений

Просмотреть файл

@ -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>