UNPKG

apeman-react-select

Version:
365 lines (322 loc) 8.5 kB
/** * apeman react package for select component. * @class ApSelect */ 'use strict' 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)