зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1354504 - Add autocomplete to network monitor search box. r=jdescottes, ntim
MozReview-Commit-ID: KojxbqOAJAQ
This commit is contained in:
Родитель
652857ca5b
Коммит
eeb5e9e9e1
|
@ -12,7 +12,7 @@ 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 } = require("../constants");
|
||||
const { FILTER_SEARCH_DELAY, FILTER_FLAGS } = require("../constants");
|
||||
const {
|
||||
getDisplayedRequestsSummary,
|
||||
getRequestFilterTypes,
|
||||
|
@ -109,6 +109,7 @@ const Toolbar = createClass({
|
|||
placeholder: SEARCH_PLACE_HOLDER,
|
||||
type: "filter",
|
||||
onChange: setRequestFilterText,
|
||||
autocompleteList: FILTER_FLAGS.map((item) => `${item}:`),
|
||||
}),
|
||||
button({
|
||||
className: toggleButtonClassName.join(" "),
|
||||
|
|
|
@ -158,6 +158,22 @@ const HEADERS = [
|
|||
}
|
||||
];
|
||||
|
||||
const HEADER_FILTERS = HEADERS
|
||||
.filter(h => h.canFilter)
|
||||
.map(h => h.filterKey || h.name);
|
||||
|
||||
const FILTER_FLAGS = [
|
||||
...HEADER_FILTERS,
|
||||
"set-cookie-domain",
|
||||
"set-cookie-name",
|
||||
"set-cookie-value",
|
||||
"mime-type",
|
||||
"larger-than",
|
||||
"is",
|
||||
"has-response-header",
|
||||
"regexp",
|
||||
];
|
||||
|
||||
const REQUESTS_WATERFALL = {
|
||||
BACKGROUND_TICKS_MULTIPLE: 5, // ms
|
||||
BACKGROUND_TICKS_SCALES: 3,
|
||||
|
@ -180,6 +196,7 @@ const general = {
|
|||
EVENTS,
|
||||
FILTER_SEARCH_DELAY: 200,
|
||||
HEADERS,
|
||||
FILTER_FLAGS,
|
||||
SOURCE_EDITOR_SYNTAX_HIGHLIGHT_MAX_SIZE: 51200, // 50 KB in bytes
|
||||
REQUESTS_WATERFALL,
|
||||
};
|
||||
|
|
|
@ -30,23 +30,8 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { HEADERS } = require("../constants");
|
||||
const { FILTER_FLAGS } = require("../constants");
|
||||
const { getFormattedIPAndPort } = require("./format-utils");
|
||||
const HEADER_FILTERS = HEADERS
|
||||
.filter(h => h.canFilter)
|
||||
.map(h => h.filterKey || h.name);
|
||||
|
||||
const FILTER_FLAGS = [
|
||||
...HEADER_FILTERS,
|
||||
"set-cookie-domain",
|
||||
"set-cookie-name",
|
||||
"set-cookie-value",
|
||||
"mime-type",
|
||||
"larger-than",
|
||||
"is",
|
||||
"has-response-header",
|
||||
"regexp",
|
||||
];
|
||||
|
||||
/*
|
||||
The function `parseFilters` is from:
|
||||
|
|
|
@ -42,6 +42,7 @@ let webpackConfig = {
|
|||
"devtools/client/framework/menu": "devtools-modules/src/menu",
|
||||
"devtools/client/framework/menu-item": path.join(__dirname, "../../client/framework/menu-item"),
|
||||
"devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"),
|
||||
"devtools/client/shared/components/autocomplete-popup": path.join(__dirname, "../../client/shared/components/autocomplete-popup"),
|
||||
"devtools/client/shared/components/reps/reps": path.join(__dirname, "../../client/shared/components/reps/reps"),
|
||||
"devtools/client/shared/components/search-box": path.join(__dirname, "../../client/shared/components/search-box"),
|
||||
"devtools/client/shared/components/splitter/draggable": path.join(__dirname, "../../client/shared/components/splitter/draggable"),
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "AutocompletePopup",
|
||||
|
||||
propTypes: {
|
||||
list: PropTypes.array.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
onItemSelected: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this.computeState(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.filter === nextProps.filter) {
|
||||
return;
|
||||
}
|
||||
this.setState(this.computeState(nextProps));
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.refs.selected) {
|
||||
this.refs.selected.scrollIntoView(false);
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
return { filteredList, selectedIndex };
|
||||
},
|
||||
|
||||
/**
|
||||
* Use this method to select the top-most item
|
||||
* This method is public, called outside of the autocomplete-popup component.
|
||||
*/
|
||||
jumpToTop() {
|
||||
this.setState({ selectedIndex: 0 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Use this method to select the bottom-most item
|
||||
* This method is public.
|
||||
*/
|
||||
jumpToBottom() {
|
||||
let selectedIndex = this.state.filteredList.length - 1;
|
||||
this.setState({ selectedIndex });
|
||||
},
|
||||
|
||||
/**
|
||||
* Increment the selected index with the provided increment value. Will cycle to the
|
||||
* beginning/end of the list if the index exceeds the list boundaries.
|
||||
* This method is public.
|
||||
*
|
||||
* @param {number} increment - No. of hops in the direction
|
||||
*/
|
||||
jumpBy(increment = 1) {
|
||||
let { filteredList, selectedIndex } = this.state;
|
||||
let nextIndex = selectedIndex + increment;
|
||||
if (increment > 0) {
|
||||
// Positive cycling
|
||||
nextIndex = nextIndex > filteredList.length - 1 ? 0 : nextIndex;
|
||||
} else if (increment < 0) {
|
||||
// Inverse cycling
|
||||
nextIndex = nextIndex < 0 ? filteredList.length - 1 : nextIndex;
|
||||
}
|
||||
this.setState({selectedIndex: nextIndex});
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit the currently selected item to the onItemSelected callback
|
||||
* This method is public.
|
||||
*/
|
||||
select() {
|
||||
if (this.refs.selected) {
|
||||
this.props.onItemSelected(this.refs.selected.textContent);
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedIndex: Number(e.target.dataset.index) }, this.select);
|
||||
},
|
||||
|
||||
render() {
|
||||
let { filteredList } = this.state;
|
||||
|
||||
return filteredList.length > 0 && dom.div(
|
||||
{ className: "devtools-autocomplete-popup devtools-monospace" },
|
||||
dom.ul(
|
||||
{ className: "devtools-autocomplete-listbox" },
|
||||
filteredList.map((item, i) => {
|
||||
let isSelected = this.state.selectedIndex == i;
|
||||
let itemClassList = ["autocomplete-item"];
|
||||
|
||||
if (isSelected) {
|
||||
itemClassList.push("autocomplete-selected");
|
||||
}
|
||||
return dom.li({
|
||||
key: i,
|
||||
"data-index": i,
|
||||
className: itemClassList.join(" "),
|
||||
ref: isSelected ? "selected" : null,
|
||||
onMouseDown: this.onMouseDown,
|
||||
}, item);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
|
@ -12,6 +12,7 @@ DIRS += [
|
|||
]
|
||||
|
||||
DevToolsModules(
|
||||
'autocomplete-popup.js',
|
||||
'frame.js',
|
||||
'h-split-box.js',
|
||||
'notification-box.css',
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
|
||||
const AutocompletePopup = createFactory(require("devtools/client/shared/components/autocomplete-popup"));
|
||||
|
||||
/**
|
||||
* A generic search box component for use across devtools
|
||||
|
@ -20,12 +21,20 @@ module.exports = createClass({
|
|||
keyShortcut: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
type: PropTypes.string,
|
||||
autocompleteList: PropTypes.array,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
autocompleteList: [],
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
value: ""
|
||||
value: "",
|
||||
focused: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -56,7 +65,9 @@ module.exports = createClass({
|
|||
|
||||
onChange() {
|
||||
if (this.state.value !== this.refs.input.value) {
|
||||
this.setState({ value: this.refs.input.value });
|
||||
this.setState({
|
||||
value: this.refs.input.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.props.delay) {
|
||||
|
@ -82,8 +93,59 @@ module.exports = createClass({
|
|||
this.onChange();
|
||||
},
|
||||
|
||||
onFocus() {
|
||||
this.setState({ focused: true });
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
this.setState({ focused: false });
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
let { autocompleteList } = this.props;
|
||||
let { autocomplete } = this.refs;
|
||||
|
||||
if (autocompleteList.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
autocomplete.jumpBy(1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
autocomplete.jumpBy(-1);
|
||||
break;
|
||||
case "PageDown":
|
||||
autocomplete.jumpBy(5);
|
||||
break;
|
||||
case "PageUp":
|
||||
autocomplete.jumpBy(-5);
|
||||
break;
|
||||
case "Enter":
|
||||
case "Tab":
|
||||
e.preventDefault();
|
||||
autocomplete.select();
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
this.onBlur();
|
||||
break;
|
||||
case "Home":
|
||||
autocomplete.jumpToTop();
|
||||
break;
|
||||
case "End":
|
||||
autocomplete.jumpToBottom();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let { type = "search", placeholder } = this.props;
|
||||
let {
|
||||
type = "search",
|
||||
placeholder,
|
||||
autocompleteList
|
||||
} = this.props;
|
||||
let { value } = this.state;
|
||||
let divClassList = ["devtools-searchbox", "has-clear-btn"];
|
||||
let inputClassList = [`devtools-${type}input`];
|
||||
|
@ -96,14 +158,27 @@ module.exports = createClass({
|
|||
dom.input({
|
||||
className: inputClassList.join(" "),
|
||||
onChange: this.onChange,
|
||||
onFocus: this.onFocus,
|
||||
onBlur: this.onBlur,
|
||||
onKeyDown: this.onKeyDown,
|
||||
placeholder,
|
||||
ref: "input",
|
||||
value
|
||||
value,
|
||||
}),
|
||||
dom.button({
|
||||
className: "devtools-searchinput-clear",
|
||||
hidden: value == "",
|
||||
onClick: this.onClearButtonClick
|
||||
}),
|
||||
autocompleteList.length > 0 && this.state.focused &&
|
||||
AutocompletePopup({
|
||||
list: autocompleteList,
|
||||
filter: value,
|
||||
ref: "autocomplete",
|
||||
onItemSelected: (itemValue) => {
|
||||
this.setState({ value: itemValue });
|
||||
this.onChange();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -82,35 +82,13 @@ html|button, html|select {
|
|||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox .autocomplete-selected {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.dark-theme .autocomplete-selected,
|
||||
.devtools-autocomplete-listbox.dark-theme .autocomplete-item:hover {
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > .autocomplete-value,
|
||||
.devtools-autocomplete-listbox:focus.dark-theme .autocomplete-selected > .initial-value {
|
||||
color: hsl(208,100%,60%);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.dark-theme .autocomplete-selected > span {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.dark-theme .autocomplete-item > span {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox .autocomplete-item > .initial-value,
|
||||
.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-value {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox .autocomplete-item > .autocomplete-count {
|
||||
|
@ -120,52 +98,60 @@ html|button, html|select {
|
|||
/* Rest of the dark and light theme */
|
||||
|
||||
.devtools-autocomplete-popup,
|
||||
.CodeMirror-hints,
|
||||
.CodeMirror-Tern-tooltip {
|
||||
border: 1px solid hsl(210,24%,90%);
|
||||
background-image: linear-gradient(to bottom, hsla(209,18%,100%,0.9), hsl(210,24%,95%));
|
||||
box-shadow: 0 1px 0 hsla(209,29%,90%,.25) inset;
|
||||
}
|
||||
|
||||
.theme-dark .devtools-autocomplete-popup,
|
||||
.theme-dark .CodeMirror-hints,
|
||||
.theme-dark .CodeMirror-Tern-tooltip {
|
||||
border: 1px solid hsl(210,11%,10%);
|
||||
background-image: linear-gradient(to bottom, hsla(209,18%,18%,0.9), hsl(210,11%,16%));
|
||||
}
|
||||
|
||||
.devtools-autocomplete-popup.light-theme,
|
||||
.light-theme .CodeMirror-hints,
|
||||
.light-theme .CodeMirror-Tern-tooltip {
|
||||
border: 1px solid hsl(210,24%,90%);
|
||||
background-image: linear-gradient(to bottom, hsla(209,18%,100%,0.9), hsl(210,24%,95%));
|
||||
}
|
||||
|
||||
.devtools-autocomplete-popup.light-theme {
|
||||
box-shadow: 0 1px 0 hsla(209,29%,90%,.25) inset;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-firebug .devtools-autocomplete-popup {
|
||||
border-color: var(--theme-splitter-color);
|
||||
border-radius: 5px;
|
||||
font-size: var(--theme-autompletion-font-size);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-popup.firebug-theme {
|
||||
background: var(--theme-body-background);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected,
|
||||
.devtools-autocomplete-listbox.firebug-theme .autocomplete-item:hover,
|
||||
.devtools-autocomplete-listbox.light-theme .autocomplete-selected,
|
||||
.devtools-autocomplete-listbox.light-theme .autocomplete-item:hover {
|
||||
.devtools-autocomplete-listbox .autocomplete-selected,
|
||||
.devtools-autocomplete-listbox .autocomplete-item:hover {
|
||||
background-color: rgba(128,128,128,0.3);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.firebug-theme .autocomplete-selected > .autocomplete-value,
|
||||
.devtools-autocomplete-listbox:focus.firebug-theme .autocomplete-selected > .initial-value,
|
||||
.devtools-autocomplete-listbox.light-theme .autocomplete-selected > .autocomplete-value,
|
||||
.devtools-autocomplete-listbox:focus.light-theme .autocomplete-selected > .initial-value {
|
||||
.theme-dark .devtools-autocomplete-listbox .autocomplete-selected,
|
||||
.theme-dark .devtools-autocomplete-listbox .autocomplete-item:hover {
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox .autocomplete-selected > .autocomplete-value,
|
||||
.devtools-autocomplete-listbox:focus .autocomplete-selected > .initial-value {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox.firebug-theme .autocomplete-item > span,
|
||||
.devtools-autocomplete-listbox.light-theme .autocomplete-item > span {
|
||||
.theme-dark .devtools-autocomplete-listbox .autocomplete-selected > .autocomplete-value,
|
||||
.theme-dark .devtools-autocomplete-listbox:focus .autocomplete-selected > .initial-value {
|
||||
color: hsl(208,100%,60%);
|
||||
}
|
||||
|
||||
.devtools-autocomplete-listbox .autocomplete-item > span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.theme-dark .devtools-autocomplete-listbox .autocomplete-item > span {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.theme-dark .devtools-autocomplete-listbox .autocomplete-selected > span {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
/* Autocomplete list clone used for accessibility. */
|
||||
|
||||
.devtools-autocomplete-list-aria-clone {
|
||||
|
@ -493,6 +479,14 @@ checkbox:-moz-focusring {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.devtools-searchbox .devtools-autocomplete-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
line-height: initial !important;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Don't add 'double spacing' for inputs that are at beginning / end
|
||||
of a toolbar (since the toolbar has it's own spacing). */
|
||||
.devtools-toolbar > .devtools-textinput:first-child,
|
||||
|
|
Загрузка…
Ссылка в новой задаче