apeman-react-select
Version:
apeman react package for select component.
365 lines (322 loc) • 8.5 kB
JSX
/**
* apeman react package for select component.
* @class ApSelect
*/
import React, { Component, PropTypes as types } from 'react'
import ReactDOM from 'react-dom'
import classnames from 'classnames'
import ApSelectItem from './ap_select_item'
import ApSelectLabel from './ap_select_label'
import numcal from 'numcal'
import { get } from 'bwindow'
import { ApLayoutMixin } from 'apeman-react-mixin-layout'
import { withOutside } from 'apeman-react-touchable'
/** @lends ApSelect */
const ApSelect = React.createClass({
// --------------------
// Specs
// --------------------
propTypes: {
/** Options of select */
options: types.object.isRequired,
/** Option elements to render */
optionElements: types.object,
/** Name of select element */
name: types.string,
/** Value of select element */
value: types.string,
/** Allow multiple select */
multiple: types.bool,
/** Handler for change event */
onChange: types.func,
/** Icon to toggle select */
openIcon: types.string,
/** Placeholder of select element */
placeholder: types.string
},
mixins: [
ApLayoutMixin
],
statics: {},
getInitialState () {
const s = this
return {
focused: false,
focusIndex: s.getIndexForValue(s.props.value)
}
},
getDefaultProps () {
return {
optionElements: null,
value: '',
name: null,
multiple: false,
onChange: null,
openIcon: 'ion ion-arrow-down-b',
placeholder: null
}
},
render () {
const s = this
let { state, props, layouts } = s
let { options, optionElements } = props
let values = s.getOptionValues()
let hasOption = options && Object.keys(options).length > 0
if (!hasOption) {
return null
}
let _option = (value) => optionElements && optionElements[ value ] || options[ value ] || null
return (
<span className={ classnames('ap-select-wrap') }>
<span className={ classnames('ap-select-options-list', {
'ap-select-options-list-visible': state.focused
}) } ref={ (list) => s.registerNode(list, 'list') }
>
<ul className='ap-select-options-list-inner'
style={ layouts.listInner }>
{ values.map((value, i) =>
<li key={ value }
value={ value }
className={ classnames('ap-select-options-list-item') }>
<ApSelectItem onTap={ s.handleItemTap }
data={ value }
focused={ state.focusIndex === i }
label={ options[ value ] }
>
{ _option(value) || null }
</ApSelectItem>
</li>
) }
</ul>
</span>
<select id={ props.id }
name={ props.name }
placeholder={ props.placeholder }
onChange={ props.onChange }
className={ classnames('ap-select', props.className) }
onFocus={ () => s.setFocus(true) }
style={ Object.assign({}, props.style) }
tabIndex="-1"
>
{ values.map((value) =>
<option key={ value } value={ value }>{ options[ value ] }</option>
) }
{ props.children }
</select>
<input type='text'
ref={ (text) => s.registerNode(text, 'text') }
className='ap-select-dummy-text'
onKeyUp={ s.handleKeyUp }
onKeyDown={ s.handleKeyDown }
onFocus={ () => s.setFocus(true) }
onBlur={ () => s.setFocus(false) }
/>
<ApSelectLabel value={ _option(props.value) }
placeholder={ props.placeholder }
icon={ props.openIcon }
onTap={ s.handleLabelTap }
/>
</span>
)
},
// --------------------
// Lifecycle
// --------------------
componentWillMount () {
const s = this
s.nodes = {}
},
componentDidMount () {
const s = this
let body = get('document.body')
body.addEventListener('click', s.handleClickForOutside)
},
componentWillUnmount () {
const s = this
let body = get('document.body')
body.removeEventListener('click', s.handleClickForOutside)
},
// ------------------
// Custom
// ------------------
moveFocusIndex (i) {
const s = this
let { state } = s
let values = s.getOptionValues()
let index = state.focusIndex + i
let over = (index === -1) || (index === values.length)
if (over) {
return
}
s.setState({
focusIndex: index
})
},
enterFocused (e) {
const s = this
let { state, props } = s
if (!state.focused) {
return
}
let values = s.getOptionValues()
let value = values[ state.focusIndex ]
s.setState({
focused: false,
focusIndex: s.getIndexForValue(value)
})
e.target.value = value
if (props.onChange) {
props.onChange(e)
}
},
getOptionValues () {
const s = this
let { props } = s
return Object.keys(props.options || {})
},
getIndexForValue (value) {
const s = this
return s.getOptionValues().indexOf(value)
},
registerNode (elm, name) {
const s = this
s.nodes[ name ] = ReactDOM.findDOMNode(elm)
},
// --------------------
// Handle
// --------------------
handleLabelTap (e) {
const s = this
let { state } = s
let { text } = s.nodes
let focused = !state.focused
if (focused) {
s.layout()
text.focus()
} else {
text.blur()
}
s.setState({
focused,
focusIndex: s.getIndexForValue(s.props.value)
})
},
setFocus (focused) {
const s = this
if (focused === s.state.focused) {
return
}
if(s._focusAt){
const fromLastFocusAt = new Date() - s._focusAt
if(fromLastFocusAt < 500){
return
}
}
s._focusAt = new Date()
s.setState({
focused
})
},
handleKeyDown (e) {
const s = this
let { props } = s
if (!s.state.focused) {
s.setState({ focused: true })
return
}
switch (e.keyCode) {
case 38: // UP
s.moveFocusIndex(-1)
break
case 40: // DOWN
s.moveFocusIndex(+1)
break
case 13: // Enter
s.enterFocused(e)
break
case 9: // Tab
break
default:
e.preventDefault()
e.stopPropagation()
break
}
if (props.onKeyDown) {
props.onKeyDown(e)
}
},
handleKeyUp (e) {
const s = this
let { props } = s
if (props.onKeyUp) {
props.onKeyUp(e)
}
e.stopPropagation()
},
handleItemTap (e) {
const s = this
let { props } = s
Object.assign(e.target, {
value: e.target.value || e.data || null,
name: e.target.name || props.name
})
if (props.onChange) {
props.onChange(e)
}
s.setState({
focused: false,
focusIndex: s.getIndexForValue(s.props.value)
})
},
handleClickForOutside (e) {
const s = this
let node = ReactDOM.findDOMNode(s)
if (!node) {
return
}
let contained = node.contains(e.target)
if (!contained) {
s.outsideDidTap(e)
}
},
outsideDidTap (e) {
const s = this
s.setFocus(false)
},
// ------------------
// ApLayoutMixin
// ------------------
getInitialLayouts () {
return {
listInner: {
transform: 'initial'
}
}
},
calcLayouts () {
const s = this
let { innerHeight, innerWidth } = window
let { list } = s.nodes
if (!list) {
return {}
}
return {
listInner: s._listInnerLayout(list.getBoundingClientRect(), innerWidth, innerHeight)
}
},
// ------------------
// Private
// ------------------
_listInnerLayout (rect, boundsWidth, boundsHeight) {
let x = numcal.min(boundsWidth - rect.right, 0)
let y = numcal.min(boundsHeight - rect.bottom, 0)
let maxHeight = `${numcal.min(boundsHeight, 280)}px`
return {
transform: `translate(${x}px, ${y}px)`,
maxHeight
}
}
})
export { ApSelect }
export default withOutside(ApSelect)