Created
May 21, 2015 19:59
-
-
Save ryanflorence/ba2f061764e0d2895259 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <div style={this.menuStyle} children={items}/> | |
}, | |
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 <div style={style}>{this.props.renderMenu(items, this.state.value)}</div> | |
} | |
getActiveItemValue () { | |
if (this.state.highlightedIndex === null) | |
return "" | |
else { | |
return this.props.getItemValue(this.props.items[this.state.highlightedIndex]) | |
} | |
} | |
render () { | |
return ( | |
<div style={{display: 'inline-block'}}> | |
<input | |
role="combobox" | |
aria-label={this.getActiveItemValue()} | |
ref="input" | |
onFocus={() => 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()} | |
</div> | |
) | |
} | |
} | |
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 [<div style={style}>{text.charAt(0)}</div>, item] | |
} | |
else { | |
return item | |
} | |
}) | |
} | |
render () { | |
return ( | |
<div style={styles.wrapper}> | |
<Autocomplete | |
initialValue="Ma" | |
items={getStates()} | |
getItemValue={(item) => item.name} | |
shouldItemRender={matchStateToTerm} | |
sortItems={sortStates} | |
renderItem={(item, isHighlighted) => ( | |
<div | |
style={isHighlighted ? { | |
color: 'white', | |
background: 'hsl(200, 50%, 50%)', | |
padding: '2px 6px' | |
} : { | |
padding: '2px 6px' | |
}} | |
key={item.abbr} | |
>{item.name}</div> | |
)} | |
/> | |
<Autocomplete | |
items={this.state.dynamicItems} | |
getItemValue={(item) => item.name} | |
onSelect={() => this.setState({ dynamicItems: [] })} | |
onChange={(value) => { | |
this.setState({loading: true}) | |
fakeRequest(value, (items) => { | |
this.setState({ dynamicItems: items, loading: false }) | |
}) | |
}} | |
renderItem={(item, isHighlighted) => ( | |
<div | |
style={isHighlighted ? { | |
color: 'white', | |
background: 'hsl(200, 50%, 50%)', | |
padding: '0 6px' | |
} : { | |
padding: '0 6px' | |
}} | |
key={item.abbr} | |
id={item.abbr} | |
>{item.name}</div> | |
)} | |
renderMenu={(items, value) => ( | |
<div> | |
{value === '' ? ( | |
<div style={{padding: 6}}>Type of the name of a United State</div> | |
) : this.state.loading ? ( | |
<div style={{padding: 6}}>Loading...</div> | |
) : items.length === 0 ? ( | |
<div style={{padding: 6}}>No matches for {value}</div> | |
) : this.renderItems(items)} | |
</div> | |
)} | |
/> | |
</div> | |
) | |
} | |
} | |
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(<App/>, document.getElementById('app')) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment