Switch to Search component in tag selector and support X/Esc to clear
This moves debouncing into the search component and adds cancel behavior from the XUL search textbox. For now, this uses the X button from Firefox.
This commit is contained in:
parent
14d25d273a
commit
42667e7090
6 changed files with 138 additions and 21 deletions
93
chrome/content/zotero/components/search.jsx
Normal file
93
chrome/content/zotero/components/search.jsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
class Search extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
state = {
|
||||
immediateValue: this.props.value
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
var prevProps = state.prevProps || {};
|
||||
return {
|
||||
prevProps: props,
|
||||
immediateValue: prevProps.value !== props.value
|
||||
? props.value
|
||||
: state.immediateValue
|
||||
};
|
||||
}
|
||||
|
||||
handleInput = (event) => {
|
||||
var value = event.target.value;
|
||||
// Update controlled value and cancel button immediately
|
||||
this.setState({
|
||||
immediateValue: value
|
||||
});
|
||||
// Debounce the search based on the timeout
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
this._timeout = this.props.timeout
|
||||
&& setTimeout(() => this.props.onSearch(value), this.props.timeout);
|
||||
}
|
||||
|
||||
handleClear = () => {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
this.setState({
|
||||
immediateValue: ''
|
||||
});
|
||||
this.props.onSearch('');
|
||||
}
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
if (event.key == 'Escape') {
|
||||
this.handleClear();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.inputRef.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="search">
|
||||
<input
|
||||
type="search"
|
||||
onInput={this.handleInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.inputRef}
|
||||
value={this.state.immediateValue}
|
||||
/>
|
||||
{this.state.immediateValue !== ''
|
||||
? <div
|
||||
className="search-cancel-button"
|
||||
onClick={this.handleClear}/>
|
||||
: ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
inputRef: PropTypes.object,
|
||||
onSearch: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onSearch: () => {},
|
||||
timeout: 300,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Search;
|
|
@ -6,6 +6,7 @@ const TagList = require('./tag-selector/tag-list');
|
|||
const Input = require('./form/input');
|
||||
const { Button } = require('./button');
|
||||
const { IconTagSelectorMenu } = require('./icons');
|
||||
const Search = require('./search');
|
||||
|
||||
class TagSelector extends React.Component {
|
||||
render() {
|
||||
|
@ -13,13 +14,11 @@ class TagSelector extends React.Component {
|
|||
<div className="tag-selector">
|
||||
<TagList {...this.props} />
|
||||
<div className="tag-selector-filter-container">
|
||||
<Input
|
||||
type="search"
|
||||
ref={ref => this.focusTextbox = ref && ref.focus}
|
||||
<Search
|
||||
value={this.props.searchString}
|
||||
onChange={this.props.onSearch}
|
||||
onSearch={this.props.onSearch}
|
||||
inputRef={this.searchBoxRef}
|
||||
className="tag-selector-filter"
|
||||
size="1"
|
||||
/>
|
||||
<Button
|
||||
icon={<IconTagSelectorMenu />}
|
||||
|
@ -46,8 +45,8 @@ TagSelector.propTypes = {
|
|||
onDragExit: PropTypes.func,
|
||||
onDrop: PropTypes.func
|
||||
}),
|
||||
searchBoxRef: PropTypes.object,
|
||||
searchString: PropTypes.string,
|
||||
shouldFocus: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
onTagContext: PropTypes.func,
|
||||
onSearch: PropTypes.func,
|
||||
|
@ -58,7 +57,6 @@ TagSelector.propTypes = {
|
|||
TagSelector.defaultProps = {
|
||||
tags: [],
|
||||
searchString: '',
|
||||
shouldFocus: false,
|
||||
onSelect: () => Promise.resolve(),
|
||||
onTagContext: () => Promise.resolve(),
|
||||
onSearch: () => Promise.resolve(),
|
||||
|
|
|
@ -29,6 +29,11 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
|
|||
this.displayAllTags = Zotero.Prefs.get('tagSelector.displayAllTags');
|
||||
this.selectedTags = new Set();
|
||||
this.state = defaults;
|
||||
this.searchBoxRef = React.createRef();
|
||||
}
|
||||
|
||||
focusTextbox() {
|
||||
this.searchBoxRef.focus();
|
||||
}
|
||||
|
||||
// Update trigger #1 (triggered by ZoteroPane)
|
||||
|
@ -177,9 +182,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
|
|||
});
|
||||
return <TagSelector
|
||||
tags={tags}
|
||||
ref={ref => this.focusTextbox = ref && ref.focusTextbox}
|
||||
searchBoxRef={this.searchBoxRef}
|
||||
searchString={this.state.searchString}
|
||||
shouldFocus={this.state.shouldFocus}
|
||||
dragObserver={this.dragObserver}
|
||||
onSelect={this.state.viewOnly ? () => {} : this.handleTagSelected}
|
||||
onTagContext={this.handleTagContext}
|
||||
|
@ -226,9 +230,9 @@ Zotero.TagSelector = class TagSelectorContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleSearch = Zotero.Utilities.debounce((searchString) => {
|
||||
handleSearch = (searchString) => {
|
||||
this.setState({searchString});
|
||||
})
|
||||
}
|
||||
|
||||
dragObserver = {
|
||||
onDragOver: function(event) {
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
// Components
|
||||
// --------------------------------------------------
|
||||
|
||||
@import "components/tag-selector";
|
||||
@import "components/button";
|
||||
@import "components/icons";
|
||||
@import "components/search";
|
||||
@import "components/tag-selector";
|
||||
|
|
21
scss/components/_search.scss
Normal file
21
scss/components/_search.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
.search {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search input {
|
||||
-moz-appearance: searchfield;
|
||||
flex: 1 0;
|
||||
min-width: 40px;
|
||||
height: 24px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.search .search-cancel-button {
|
||||
background-image: url(chrome://global/skin/icons/searchfield-cancel.svg);
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
cursor: default;
|
||||
}
|
|
@ -24,6 +24,13 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.tag-selector-list {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag-selector-filter-container {
|
||||
height: auto;
|
||||
flex: 0 0 1em;
|
||||
|
@ -32,16 +39,9 @@
|
|||
padding: 0.125em 0 0.125em 0.5em;
|
||||
}
|
||||
|
||||
.tag-selector-list {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag-selector-filter {
|
||||
.tag-selector-filter-container .search {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.tag-selector-actions {
|
||||
|
|
Loading…
Reference in a new issue