2015-10-12 13:57:37 +02:00
|
|
|
'use strict';
|
|
|
|
|
2015-10-12 17:33:07 +02:00
|
|
|
import React from 'react';
|
2015-10-12 13:57:37 +02:00
|
|
|
|
|
|
|
import Input from 'react-bootstrap/lib/Input';
|
|
|
|
import Glyphicon from 'react-bootstrap/lib/Glyphicon';
|
|
|
|
import { getLangText } from '../utils/lang_utils';
|
|
|
|
|
|
|
|
|
|
|
|
const { func, string, number } = React.PropTypes;
|
|
|
|
|
|
|
|
const SearchBar = React.createClass({
|
|
|
|
propTypes: {
|
2015-10-12 17:10:04 +02:00
|
|
|
// a function that accepts a string as a search query and updates the
|
|
|
|
// propagated `searchQuery` after successfully retrieving the
|
|
|
|
// request from the server
|
2015-10-12 13:57:37 +02:00
|
|
|
searchFor: func.isRequired,
|
2015-10-12 16:38:00 +02:00
|
|
|
searchQuery: string.isRequired,
|
|
|
|
|
2015-10-12 13:57:37 +02:00
|
|
|
className: string,
|
|
|
|
|
2015-10-12 17:10:04 +02:00
|
|
|
// the number of milliseconds the component
|
|
|
|
// should wait before requesting search results from the server
|
2015-10-12 13:57:37 +02:00
|
|
|
threshold: number.isRequired
|
|
|
|
},
|
|
|
|
|
|
|
|
getInitialState() {
|
|
|
|
return {
|
2015-10-12 17:33:07 +02:00
|
|
|
timer: null,
|
2015-10-12 16:38:00 +02:00
|
|
|
searchQuery: '',
|
2015-10-12 17:10:04 +02:00
|
|
|
loading: false
|
2015-10-12 13:57:37 +02:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2015-10-12 16:38:00 +02:00
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
const searchQueryProps = this.props.searchQuery;
|
|
|
|
const searchQueryPrevProps = prevProps.searchQuery;
|
|
|
|
const searchQueryState = this.state.searchQuery;
|
2015-10-12 17:10:04 +02:00
|
|
|
const { loading } = this.state;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 1. Condition: `loading` must be true, which implies that `evaluateTimer`,
|
|
|
|
* has already been called
|
|
|
|
*
|
|
|
|
* AND
|
|
|
|
*
|
2015-10-12 17:12:10 +02:00
|
|
|
* (
|
2015-10-12 17:10:04 +02:00
|
|
|
* 2. Condition: `searchQueryProps` and `searchQueryState` are true and equal
|
|
|
|
* (which means that the search query has been propagated to the inner
|
|
|
|
* fetch method of `fetchPieceList`, which in turn means that a fetch
|
|
|
|
* has completed)
|
|
|
|
*
|
|
|
|
* OR
|
|
|
|
*
|
|
|
|
* 3. Condition: `searchQueryProps` and `searchQueryState` can be any value (`true` or
|
|
|
|
* `false`, as long as they're equal). This case only occurs when the user
|
|
|
|
* has entered a `searchQuery` and deletes the query in one go, reseting
|
|
|
|
* `searchQueryProps` to empty string ('' === false) again.
|
2015-10-12 17:12:10 +02:00
|
|
|
* )
|
2015-10-12 17:10:04 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
const firstCondition = !!loading;
|
|
|
|
const secondCondition = searchQueryProps && searchQueryState && searchQueryProps === searchQueryState;
|
|
|
|
const thirdCondition = !searchQueryPrevProps && searchQueryProps === searchQueryState;
|
|
|
|
|
|
|
|
if(firstCondition && (secondCondition || thirdCondition)) {
|
|
|
|
this.setState({ loading: false });
|
2015-10-12 16:38:00 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-10-13 17:28:10 +02:00
|
|
|
componentWillReceiveProps(nextProps) {
|
|
|
|
/**
|
|
|
|
* This enables the `PieceListStore` to override the state
|
|
|
|
* of that component in case someone is changing the `searchQuery` on
|
|
|
|
* another component.
|
|
|
|
*
|
|
|
|
* Like how it's being done in the 'Clear search' dialog.
|
|
|
|
*/
|
2015-10-13 17:30:49 +02:00
|
|
|
if(this.props.searchQuery !== nextProps.searchQuery || !this.state.searchQuery) {
|
2015-10-13 17:28:10 +02:00
|
|
|
this.setState({ searchQuery: nextProps.searchQuery });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-10-12 17:33:07 +02:00
|
|
|
startTimer(searchQuery) {
|
|
|
|
const { timer } = this.state;
|
2015-10-12 13:57:37 +02:00
|
|
|
const { threshold } = this.props;
|
2015-10-12 17:10:04 +02:00
|
|
|
|
2015-10-12 17:33:07 +02:00
|
|
|
// The timer waits for the specified threshold time in milliseconds
|
|
|
|
// and then calls `evaluateTimer`.
|
|
|
|
// If another letter has been called in the mean time (timespan < `threshold`),
|
|
|
|
// the present timer gets cleared and a new one is added to `this.state`.
|
|
|
|
// This means that `evaluateTimer`, will only be called when the threshold has actually
|
|
|
|
// passed,
|
|
|
|
clearTimeout(timer); // apparently `clearTimeout` can be called with null, without throwing errors
|
|
|
|
const newTimer = setTimeout(this.evaluateTimer(searchQuery), threshold);
|
2015-10-12 13:57:37 +02:00
|
|
|
|
2015-10-12 17:33:07 +02:00
|
|
|
this.setState({ timer: newTimer });
|
2015-10-12 13:57:37 +02:00
|
|
|
},
|
|
|
|
|
2015-10-12 17:33:07 +02:00
|
|
|
evaluateTimer(searchQuery) {
|
2015-10-12 16:38:00 +02:00
|
|
|
return () => {
|
2015-10-12 17:33:07 +02:00
|
|
|
this.setState({ timer: null, loading: true }, () => {
|
|
|
|
// search for the query
|
|
|
|
this.props.searchFor(searchQuery);
|
|
|
|
});
|
2015-10-12 16:38:00 +02:00
|
|
|
};
|
2015-10-12 13:57:37 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
handleChange({ target: { value }}) {
|
2015-10-12 17:10:04 +02:00
|
|
|
// On each letter entry we're updating the state of the component
|
|
|
|
// and start a timer, which we're also pushing to the state
|
|
|
|
// of the component
|
2015-10-12 17:33:07 +02:00
|
|
|
this.startTimer(value);
|
2015-10-12 16:38:00 +02:00
|
|
|
this.setState({ searchQuery: value });
|
2015-10-12 13:57:37 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
render() {
|
2015-10-12 16:38:00 +02:00
|
|
|
let searchIcon = <Glyphicon glyph='search' className="filter-glyph"/>;
|
2015-10-12 13:57:37 +02:00
|
|
|
const { className } = this.props;
|
2015-10-13 17:30:49 +02:00
|
|
|
const { loading, searchQuery } = this.state;
|
2015-10-12 16:38:00 +02:00
|
|
|
|
2015-10-12 17:10:04 +02:00
|
|
|
if(loading) {
|
2015-10-12 16:38:00 +02:00
|
|
|
searchIcon = <span className="glyph-ascribe-spool-chunked ascribe-color spin"/>;
|
|
|
|
}
|
2015-10-12 13:57:37 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
<span className={className}>
|
|
|
|
<Input
|
|
|
|
type='text'
|
2015-10-13 17:30:49 +02:00
|
|
|
value={searchQuery}
|
2015-10-12 13:57:37 +02:00
|
|
|
placeholder={getLangText('Search%s', '...')}
|
|
|
|
onChange={this.handleChange}
|
|
|
|
addonAfter={searchIcon} />
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default SearchBar;
|