appirio-tech-react-components-test10
Version:
Topcoder UI Kit library of components for our React apps.
260 lines (232 loc) • 8.43 kB
JSX
require('./SearchBar.scss')
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import SearchSuggestions from '../SearchSuggestions/SearchSuggestions'
import Loader from '../Loader/Loader'
import classNames from 'classnames'
//states: empty, filled, focused
class SearchBar extends Component {
constructor(props) {
super(props)
const initialTerm = this.getQueryStringValue(props.searchTermKey)
this.state = {
searchState: initialTerm.length > 0 ? 'filled' : 'empty',
suggestions: [],
searchValue: initialTerm,
finalTerm: initialTerm
}
this.onFocus = this.onFocus.bind(this)
this.onChange = this.onChange.bind(this)
this.onKeyUp = this.onKeyUp.bind(this)
this.clearSearch = this.clearSearch.bind(this)
this.handleClick = this.handleClick.bind(this)
this.search = this.search.bind(this)
this.handleSuggestionSelect = this.handleSuggestionSelect.bind(this)
this.handleOutsideClick = this.handleOutsideClick.bind(this)
this.handleSuggestionsUpdate = this.handleSuggestionsUpdate.bind(this)
}
getQueryStringValue (key) {
return unescape(window.location.href.replace(new RegExp('^(?:.*[&\\?]' + escape(key).replace(/[\.\+\*]/g, '\\$&') + '(?:\\=([^&]*))?)?.*$', 'i'), '$1'))
}
componentDidMount() {
window.addEventListener('click', this.handleOutsideClick)
}
componentWillUnmount() {
window.removeEventListener('click', this.handleOutsideClick)
}
componentWillReceiveProps(nextProps) {
const searchVal = this.getQueryStringValue(nextProps.searchTermKey)
if (searchVal !== this.state.searchValue) {
this.setState({ searchState: 'filled', searchValue: searchVal })
}
}
handleOutsideClick(evt) {
let t = evt.target
let i = 0
const searchBarNode = ReactDOM.findDOMNode(this)
let clickedInside = false
while(t !== null && i < 10) {
i++
if (t === searchBarNode) {
clickedInside = true
break
}
t = t.parentNode
}
if (!clickedInside) {
if(this.state.searchValue) {
this.setState({ searchState: 'filled' })
} else {
if (this.state.finalTerm && this.state.finalTerm.trim().length > 0) {
this.setState({ searchState: 'filled', searchValue: this.state.finalTerm })
} else {
this.setState({ searchState: 'empty' })
}
}
}
}
onFocus() {
this.setState({ searchState: 'focused' })
}
blur() {
this.refs.searchValue.blur()
}
handleSuggestionsUpdate(requestNo, data) {
if (requestNo === this.state.maxRequestNo) {
console.log('SUGGESTIONS', data)
this.setState({loading: false, suggestions: data, selectedSuggestionIdx: null})
}
}
onChange() {
const oldTerm = this.state.searchValue
const newTerm = this.refs.searchValue.value ? this.refs.searchValue.value.trim() : ''
this.setState(
function(prevState) {
const rc = prevState.requestNo ? prevState.requestNo + 1 : 1
return {
searchValue: this.refs.searchValue.value,
requestNo: rc,
maxRequestNo: rc,
loading: newTerm.length > 0,
searchState: 'focused'
}
},
function() {
if (newTerm.length > 0) {
this.props.onTermChange.apply(null, [
oldTerm,
newTerm,
this.state.requestNo,
this.handleSuggestionsUpdate
])
}
}
)
}
clearSearch() {
this.refs.searchValue.value = ''
this.setState({ searchValue: this.refs.searchValue.value, finalTerm: '' })
this.setState({ searchState: 'empty' })
this.setState({ suggestions: false })
this.props.onClearSearch()
}
onKeyUp(evt) {
const eventKey = evt.keyCode
evt.stopPropagation()
evt.preventDefault()
// if return is pressed
if (eventKey === 13) {
this.setState({ searchState: 'filled', finalTerm: this.state.searchValue }, function() {
this.search()
this.blur()
})
} else if (eventKey === 39) { // right arrow key is pressed
const suggestion = this.state.suggestions[0]
if (suggestion) {
this.refs.searchValue.value = suggestion
// trigger the change event handler
this.onChange()
}
} else if (eventKey === 38) { // up arrow key
const currSelectedIdx = this.state.selectedSuggestionIdx
if (currSelectedIdx) { // index is none of (undefined, null, 0)
const suggestionIdx = currSelectedIdx - 1
const suggestion = this.state.suggestions[suggestionIdx]
this.refs.searchValue.value = suggestion
this.setState({
selectedSuggestionIdx : suggestionIdx,
searchValue: suggestion
})
}
} else if (eventKey === 40) { // down arrow key
const currSelectedIdx = this.state.selectedSuggestionIdx
// index is none of (undefined, null, 0)
if (typeof currSelectedIdx === 'undefined'
|| currSelectedIdx === null
|| this.state.suggestions.length > currSelectedIdx + 1) {
const suggestionIdx = typeof currSelectedIdx === 'number' ? currSelectedIdx + 1 : 0
const suggestion = this.state.suggestions[suggestionIdx]
this.refs.searchValue.value = suggestion
this.setState({
selectedSuggestionIdx: suggestionIdx,
searchValue: suggestion
})
}
}
}
handleSuggestionSelect(selectedTerm) {
this.setState({ searchValue: selectedTerm, searchState: 'filled' }, function() {
this.search()
})
}
search() {
const searchTerm = this.state.finalTerm ? this.state.finalTerm.trim() : ''
if(searchTerm.length > 0) {
this.props.onSearch.apply(this, [searchTerm])
}
}
handleClick() {
if(this.state.searchValue.length > 0) {
this.setState({ searchState: 'filled', finalTerm: this.state.searchValue }, () => {
this.search()
})
}
}
render() {
const recentList = this.props.recentTerms
const popularList = this.state.suggestions
const searchState = this.state.searchState
const searchValue = this.state.searchValue
let typeaheadText = ''
if(searchValue) {
for(let i = 0; i < popularList.length; i++) {
const idx = popularList[i].toLowerCase().indexOf(searchValue.toLowerCase())
// show typeahead hint only if the search term matched at 0 index
if(!typeaheadText && idx === 0) {
typeaheadText = searchValue + popularList[i].substring(searchValue.length)
}
}
} else {
typeaheadText = ''
}
const sbClasses = classNames('SearchBar', {
'state-empty' : searchState === 'empty',
'state-focused': searchState === 'focused',
'state-filled' : searchState === 'filled'
})
const results = this.state.loading === true
? <div className="loading-suggestions"><Loader /></div>
: <SearchSuggestions hideSuggestionsWhenEmpty={ this.props.hideSuggestionsWhenEmpty } recentSearch={ recentList } searchTerm={ this.state.searchValue } popularSearch={ popularList } showPopularSearchHeader={ this.props.showPopularSearchHeader } onSuggestionSelect={ this.handleSuggestionSelect } />
return (
<div className={ sbClasses }>
<span className="search-typeahead-text">{ typeaheadText }</span>
<input className="search-bar__text" onFocus={ this.onFocus } onChange={ this.onChange } onKeyUp={ this.onKeyUp } ref="searchValue" value={this.state.searchValue} />
<img className="search-bar__clear" src={ require('./x-mark.svg') } onClick={ this.clearSearch }/>
<div className="search-icon-wrap" onClick={ this.handleClick }>
<span className="search-txt">Search</span>
</div>
<div className="suggestions-panel">
{results}
</div>
</div>
)
}
}
SearchBar.propTypes = {
hideSuggestionsWhenEmpty: PropTypes.bool,
onSearch : PropTypes.func.isRequired,
onClearSearch : PropTypes.func,
onTermChange : PropTypes.func.isRequired,
recentTerms : PropTypes.array,
searchTermKey: PropTypes.string,
showPopularSearchHeader: PropTypes.bool
}
SearchBar.defaultProps = {
hideSuggestionsWhenEmpty: false,
recentTerms : [],
searchTermKey : 'q',
onClearSearch : () => {},
showPopularSearchHeader: true
}
export default SearchBar