Kevnz
5/26/2015 - 7:04 AM

exercise.js

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'))