UNPKG

react-widgets

Version:
331 lines (278 loc) 9.9 kB
/** React.DOM */ 'use strict'; var React = require('react') , cx = require('../util/cx') , _ = require('lodash') , $ = require('../util/dom') , directions = require('../util/constants').directions , mergeIntoProps = require('../util/transferProps').mergeIntoProps , SelectInput = require('./search-input') , TagList = require('./tag-list') , Popup = require('../popup/popup') , List = require('../common/list'); var btn = require('../common/btn') , propTypes = { data: React.PropTypes.array, value: React.PropTypes.array, onChange: React.PropTypes.func, valueField: React.PropTypes.string, textField: React.PropTypes.string, tagComponent: React.PropTypes.func, itemComponent: React.PropTypes.func, duration: React.PropTypes.number, //popup placeholder: React.PropTypes.string, disabled: React.PropTypes.oneOfType([ React.PropTypes.bool, React.PropTypes.array, React.PropTypes.oneOf(['disabled']) ]), readOnly: React.PropTypes.oneOfType([ React.PropTypes.bool, React.PropTypes.array, React.PropTypes.oneOf(['readonly']) ]), messages: React.PropTypes.shape({ open: React.PropTypes.string, emptyList: React.PropTypes.string, emptyFilter: React.PropTypes.string }) }; module.exports = React.createClass({ displayName: 'Select', mixins: [ require('../mixins/DataHelpersMixin'), require('../mixins/DataFilterMixin'), require('../mixins/RtlParentContextMixin'), require('../mixins/DataIndexStateMixin')('focusedIndex') ], propTypes: propTypes, getDefaultProps: function(){ return { data: [], filter: 'startsWith', messages: { emptyList: "There are no items in this list", emptyFilter: "The filter returned no results" } } }, getInitialState: function(){ var values = this.props.value == null ? [] : [].concat(this.props.value) return { open: false, processedData: this.process(this.props.data, this.props.value, ''), focusedIndex: 0, dataItems: _.map(values, function(item){ return this._dataItem(this.props.data, item) }, this) } }, componentWillReceiveProps: function(nextProps) { var values = nextProps.value == null ? [] : [].concat(nextProps.value) , items = this.process( nextProps.data , nextProps.value , this.state.searchTerm) this.setState({ //searchTerm: '', processedData: items, dataItems: _.map(values, function(item){ return this._dataItem(nextProps.data, item) }, this) }) }, render: function(){ var enabled = !(this.props.disabled === true || this.props.readOnly === true) , listID = this._id('_listbox') , optID = this._id('_option') , items = this._data() , values = this.state.dataItems; return mergeIntoProps( _.omit(this.props, _.keys(propTypes)), React.DOM.div({ref: "element", onKeyDown: this._maybeHandle(this._keyDown), onFocus: this._maybeHandle(_.partial(this._focus, true), true), onBlur: _.partial(this._focus, false), tabIndex: "-1", className: cx({ 'rw-select-list': true, 'rw-widget': true, 'rw-state-focus': this.state.focused, 'rw-state-disabled': this.props.disabled === true, 'rw-state-readonly': this.props.readOnly === true, 'rw-open': this.state.open, 'rw-rtl': this.isRtl() })}, React.DOM.div({className: "rw-select-wrapper", onClick: this._maybeHandle(this._click)}, this.props.busy && React.DOM.i({className: "rw-i rw-loading"}), TagList({ ref: "tagList", value: values, textField: this.props.textField, valueField: this.props.valueField, valueComponent: this.props.tagComponent, disabled: this.props.disabled, readOnly: this.props.readOnly, onDelete: this._delete}), SelectInput({ ref: "input", 'aria-activedescendent': this.state.open ? optID : undefined, 'aria-expanded': this.state.open, 'aria-busy': !!this.props.busy, 'aria-owns': listID, 'aria-haspopup': true, value: this.state.searchTerm, disabled: this.props.disabled === true, readOnly: this.props.readOnly === true, placeholder: this._placeholder(), onChange: this._typing}) ), Popup({open: this.state.open, onRequestClose: this.close, duration: this.props.duration}, React.DOM.div(null, List({ref: "list", id: listID, optID: optID, 'aria-autocomplete': "list", 'aria-hidden': !this.state.open, style: { maxHeight: 200, height: 'auto'}, data: items, textField: this.props.textField, valueField: this.props.valueField, focusedIndex: this.state.focusedIndex, onSelect: this._maybeHandle(this._onSelect), listItem: this.props.itemComponent, messages: { emptyList: this.props.data.length ? this.props.messages.emptyFilter : this.props.messages.emptyList }}) ) ) ) ) }, _data: function(){ return this.state.processedData }, _delete: function(value){ this._focus(true) this.change( _.without(this.state.dataItems, value)) }, _click: function(e){ this._focus(true) !this.state.open && this.open() }, _focus: function(focused, e){ var self = this; if (this.props.disabled === true ) return clearTimeout(self.timer) self.timer = setTimeout(function(){ if(focused) self.refs.input.focus() else { self.close() self.refs.tagList.clear() } if( focused !== self.state.focused) self.setState({ focused: focused }) }, 0) }, _typing: function(e){ var items = this.process(this.props.data, this.props.value, e.target.value); this.setState({ searchTerm: e.target.value, processedData: items, open: this.state.open || (this.state.open === false), focusedIndex: items.length >= this.state.focusedIndex ? 0 : this.state.focusedIndex }) }, _onSelect: function(data){ if( data === undefined ) return //handle custom tags maybe here? this.change(this.state.dataItems.concat(data)) this.close() this._focus(true) }, _keyDown: function(e){ var key = e.key , alt = e.altKey , searching = !!this.state.searchTerm , isOpen = this.state.open; if ( key === 'ArrowDown') { if ( isOpen ) this.setFocusedIndex(this.nextFocusedIndex()) else this.open() } else if ( key === 'ArrowUp') { if ( alt) this.close() else if ( isOpen ) this.setFocusedIndex( this.prevFocusedIndex()) } else if ( key === 'End'){ if ( isOpen ) this.setFocusedIndex(this._data().length - 1) else this.refs.tagList.last() } else if ( key === 'Home'){ if ( isOpen ) this.setFocusedIndex(0) else this.refs.tagList.first() } else if ( isOpen && key === 'Enter' ) this._onSelect(this._data()[this.state.focusedIndex]) else if ( key === 'Escape') isOpen ? this.close() : this.refs.tagList.clear() else if ( !searching && key === 'ArrowLeft') this.refs.tagList.prev() else if ( !searching && key === 'ArrowRight') this.refs.tagList.next() else if ( !searching && key === 'Delete') this.refs.tagList.removeCurrent() else if ( !searching && key === 'Backspace') this.refs.tagList.removeNext() }, change: function(data){ var change = this.props.onChange if ( change ) change(data) }, open: function(){ if (!(this.props.disabled === true || this.props.readOnly === true)) this.setState({ open: true }) }, close: function(){ this.setState({ open: false }) }, toggle: function(e){ this.state.open ? this.close() : this.open() }, process: function(data, values, searchTerm){ var items = _.reject(data, function(i){ return _.any( values , _.partial(this._valueMatcher, i) , this) }, this) if( searchTerm) items = this.filter(items, searchTerm) return items }, _placeholder: function(){ return (this.props.value || []).length ? '' : (this.props.placeholder || '') }, _maybeHandle: function(handler, disabledOnly){ if ( !(this.props.disabled === true || (!disabledOnly && this.props.readOnly === true))) return handler }, _id: function(suffix){ this._id_ || (this._id_ = _.uniqueId('rw_')) return (this.props.id || this._id_) + suffix } })