Skip to content

Instantly share code, notes, and snippets.

@mech
Forked from ryanflorence/exercise.js
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mech/3fc30a2358d7745fc8a5 to your computer and use it in GitHub Desktop.
Save mech/3fc30a2358d7745fc8a5 to your computer and use it in GitHub Desktop.
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