import React from 'react' import assign from 'object-assign' var styles = {} class Autocomplete extends React.Component { static propTypes = { initialValue: React.PropTypes.any, onChange: React.PropTypes.func, shouldItemRender: React.PropTypes.func, renderItem: React.PropTypes.func.isRequired, menuStyle: React.PropTypes.object } static defaultProps = { onChange () {}, renderMenu (items, value) { return
}, shouldItemRender () { return true }, sortItems () { return 0 }, menuStyle: { borderRadius: '3px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)', background: 'rgba(255, 255, 255, 0.9)', padding: '2px 0', fontSize: '90%' } } constructor (props, context) { super(props, context) this.state = { value: this.props.initialValue || '', isOpen: false, highlightedIndex: null, performAutoCompleteOnKeyUp: false, // stateful DOM yeargh! performAutoCompleteOnUpdate: false, // stateful DOM yeargh! } } componentWillReceiveProps () { this.setState({ performAutoCompleteOnUpdate: true }) } componentDidUpdate (prevProps, prevState) { if (this.state.isOpen === true && prevState.isOpen === false) this.setMenuPositions() if (this.state.isOpen && this.state.performAutoCompleteOnUpdate) { this.setState({ performAutoCompleteOnUpdate: false }, () => { this.maybeAutoCompleteText() }) } } handleKeyDown (event) { if (this.keyDownHandlers[event.key]) this.keyDownHandlers[event.key].call(this, event) else this.setState({ highlightedIndex: null, isOpen: true }) } handleChange (event) { this.setState({ value: event.target.value, performAutoCompleteOnKeyUp: true }, () => { this.props.onChange(this.state.value) }) } handleKeyUp () { if (this.state.performAutoCompleteOnKeyUp) { this.setState({ performAutoCompleteOnKeyUp: false }, () => { this.maybeAutoCompleteText() }) } } keyDownHandlers = { ArrowDown () { event.preventDefault() var { highlightedIndex } = this.state var index = ( highlightedIndex === null || highlightedIndex === this.getFilteredItems().length - 1 ) ? 0 : highlightedIndex + 1 this.setState({ highlightedIndex: index, isOpen: true, performAutoCompleteOnKeyUp: true }) }, ArrowUp (event) { event.preventDefault() var { highlightedIndex } = this.state var index = ( highlightedIndex === 0 || highlightedIndex === null ) ? this.getFilteredItems().length - 1 : highlightedIndex - 1 this.setState({ highlightedIndex: index, isOpen: true, performAutoCompleteOnKeyUp: true }) }, Enter (event) { if (this.state.highlightedIndex == null) { // hit enter after focus but before doing anything so no autocomplete attempt yet this.setState({ isOpen: false }, () => { React.findDOMNode(this.refs.input).select() }) } else { this.setState({ value: this.props.getItemValue( this.getFilteredItems()[this.state.highlightedIndex] ), isOpen: false, highlightedIndex: 0 }, () => { React.findDOMNode(this.refs.input).select() }) } }, Escape (event) { this.setState({ highlightedIndex: null, isOpen: false }) } } getFilteredItems () { return this.props.items.filter((item) => ( this.props.shouldItemRender(item, this.state.value) )).sort((a, b) => ( this.props.sortItems(a, b, this.state.value) )) } maybeAutoCompleteText () { if (this.state.value === '') return var { highlightedIndex } = this.state var items = this.getFilteredItems() if (items.length === 0) return var matchedItem = highlightedIndex !== null ? items[highlightedIndex] : items[0] var itemValue = this.props.getItemValue(matchedItem) var itemValueDoesMatch = (itemValue.toLowerCase().indexOf( this.state.value.toLowerCase() ) === 0) if (itemValueDoesMatch) { var node = React.findDOMNode(this.refs.input) var setSelection = () => { node.value = itemValue node.setSelectionRange(this.state.value.length, itemValue.length) } if (highlightedIndex === null) this.setState({ highlightedIndex: 0 }, setSelection) else setSelection() } } setMenuPositions () { var node = React.findDOMNode(this.refs.input) var rect = node.getBoundingClientRect() var computedStyle = getComputedStyle(node); var marginBottom = parseInt(computedStyle.marginBottom, 10); var marginLeft = parseInt(computedStyle.marginLeft, 10); var marginRight = parseInt(computedStyle.marginRight, 10); this.setState({ menuTop: rect.bottom + marginBottom, menuLeft: rect.left + marginLeft, menuWidth: rect.width + marginLeft + marginRight }) } renderMenu () { var items = this.getFilteredItems().map((item, index) => ( this.props.renderItem(item, this.state.highlightedIndex === index) )) var style = assign({ left: this.state.menuLeft, top: this.state.menuTop, minWidth: this.state.menuWidth, position: 'fixed', }, this.props.menuStyle) return
{this.props.renderMenu(items, this.state.value)}
} getActiveItemValue () { if (this.state.highlightedIndex === null) return "" else { return this.props.getItemValue(this.props.items[this.state.highlightedIndex]) } } render () { return (
this.setState({ isOpen: true })} onBlur={() => this.setState({ isOpen: false, highlightedIndex: null })} onChange={this.handleChange.bind(this)} onKeyDown={this.handleKeyDown.bind(this)} onKeyUp={this.handleKeyUp.bind(this)} value={this.state.value} /> {this.state.isOpen && this.renderMenu()}
) } } class App extends React.Component { constructor (props, context) { super(props, context) this.state = { dynamicItems: [] } } renderItems (items) { return items.map((item, index) => { var text = item.props.children if (index === 0 || items[index - 1].props.children.charAt(0) !== text.charAt(0)) { var style = { background: '#eee', color: '#454545', padding: '2px 6px', fontWeight: 'bold' } return [
{text.charAt(0)}
, item] } else { return item } }) } render () { return (
item.name} shouldItemRender={matchStateToTerm} sortItems={sortStates} renderItem={(item, isHighlighted) => (
{item.name}
)} /> item.name} onSelect={() => this.setState({ dynamicItems: [] })} onChange={(value) => { this.setState({loading: true}) fakeRequest(value, (items) => { this.setState({ dynamicItems: items, loading: false }) }) }} renderItem={(item, isHighlighted) => (
{item.name}
)} renderMenu={(items, value) => (
{value === '' ? (
Type of the name of a United State
) : this.state.loading ? (
Loading...
) : items.length === 0 ? (
No matches for {value}
) : this.renderItems(items)}
)} />
) } } function matchStateToTerm (state, value) { return ( state.name.toLowerCase().indexOf(value.toLowerCase()) !== -1 || state.abbr.toLowerCase().indexOf(value.toLowerCase()) !== -1 ) } function sortStates (a, b, value) { return ( a.name.toLowerCase().indexOf(value.toLowerCase()) > b.name.toLowerCase().indexOf(value.toLowerCase()) ? 1 : -1 ) } function fakeRequest (value, cb) { var items = getStates().filter((state) => { return matchStateToTerm(state, value) }).sort((a, b) => { return sortStates(a, b, value) }) setTimeout(() => { cb(items) }, 500) } function getStates() { return [ { abbr: "AL", name: "Alabama"}, { abbr: "AK", name: "Alaska"}, { abbr: "AZ", name: "Arizona"}, { abbr: "AR", name: "Arkansas"}, { abbr: "CA", name: "California"}, { abbr: "CO", name: "Colorado"}, { abbr: "CT", name: "Connecticut"}, { abbr: "DE", name: "Delaware"}, { abbr: "FL", name: "Florida"}, { abbr: "GA", name: "Georgia"}, { abbr: "HI", name: "Hawaii"}, { abbr: "ID", name: "Idaho"}, { abbr: "IL", name: "Illinois"}, { abbr: "IN", name: "Indiana"}, { abbr: "IA", name: "Iowa"}, { abbr: "KS", name: "Kansas"}, { abbr: "KY", name: "Kentucky"}, { abbr: "LA", name: "Louisiana"}, { abbr: "ME", name: "Maine"}, { abbr: "MD", name: "Maryland"}, { abbr: "MA", name: "Massachusetts"}, { abbr: "MI", name: "Michigan"}, { abbr: "MN", name: "Minnesota"}, { abbr: "MS", name: "Mississippi"}, { abbr: "MO", name: "Missouri"}, { abbr: "MT", name: "Montana"}, { abbr: "NE", name: "Nebraska"}, { abbr: "NV", name: "Nevada"}, { abbr: "NH", name: "New Hampshire"}, { abbr: "NJ", name: "New Jersey"}, { abbr: "NM", name: "New Mexico"}, { abbr: "NY", name: "New York"}, { abbr: "NC", name: "North Carolina"}, { abbr: "ND", name: "North Dakota"}, { abbr: "OH", name: "Ohio"}, { abbr: "OK", name: "Oklahoma"}, { abbr: "OR", name: "Oregon"}, { abbr: "PA", name: "Pennsylvania"}, { abbr: "RI", name: "Rhode Island"}, { abbr: "SC", name: "South Carolina"}, { abbr: "SD", name: "South Dakota"}, { abbr: "TN", name: "Tennessee"}, { abbr: "TX", name: "Texas"}, { abbr: "UT", name: "Utah"}, { abbr: "VT", name: "Vermont"}, { abbr: "VA", name: "Virginia"}, { abbr: "WA", name: "Washington"}, { abbr: "WV", name: "West Virginia"}, { abbr: "WI", name: "Wisconsin"}, { abbr: "WY", name: "Wyoming"} ] } React.render(, document.getElementById('app'))