UNPKG

react-typeahead

Version:

React-based typeahead and typeahead-tokenizer

225 lines (206 loc) 6.96 kB
var Accessor = require('../accessor'); var React = require('react'); var Token = require('./token'); var KeyEvent = require('../keyevent'); var Typeahead = require('../typeahead'); var classNames = require('classnames'); var createReactClass = require('create-react-class'); var PropTypes = require('prop-types'); function _arraysAreDifferent(array1, array2) { if (array1.length != array2.length){ return true; } for (var i = array2.length - 1; i >= 0; i--) { if (array2[i] !== array1[i]){ return true; } } } /** * A typeahead that, when an option is selected, instead of simply filling * the text entry widget, prepends a renderable "token", that may be deleted * by pressing backspace on the beginning of the line with the keyboard. */ var TypeaheadTokenizer = createReactClass({ propTypes: { name: PropTypes.string, options: PropTypes.array, customClasses: PropTypes.object, allowCustomValues: PropTypes.number, defaultSelected: PropTypes.array, initialValue: PropTypes.string, placeholder: PropTypes.string, disabled: PropTypes.bool, inputProps: PropTypes.object, onTokenRemove: PropTypes.func, onKeyDown: PropTypes.func, onKeyPress: PropTypes.func, onKeyUp: PropTypes.func, onTokenAdd: PropTypes.func, onFocus: PropTypes.func, onBlur: PropTypes.func, filterOption: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]), searchOptions: PropTypes.func, displayOption: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]), formInputOption: PropTypes.oneOfType([ PropTypes.string, PropTypes.func ]), maxVisible: PropTypes.number, resultsTruncatedMessage: PropTypes.string, defaultClassNames: PropTypes.bool, showOptionsWhenEmpty: PropTypes.bool, }, getInitialState: function() { return { // We need to copy this to avoid incorrect sharing // of state across instances (e.g., via getDefaultProps()) selected: this.props.defaultSelected.slice(0) }; }, getDefaultProps: function() { return { options: [], defaultSelected: [], customClasses: {}, allowCustomValues: 0, initialValue: "", placeholder: "", disabled: false, inputProps: {}, defaultClassNames: true, filterOption: null, searchOptions: null, displayOption: function(token){ return token }, formInputOption: null, onKeyDown: function(event) {}, onKeyPress: function(event) {}, onKeyUp: function(event) {}, onFocus: function(event) {}, onBlur: function(event) {}, onTokenAdd: function() {}, onTokenRemove: function() {}, showOptionsWhenEmpty: false, }; }, componentWillReceiveProps: function(nextProps){ // if we get new defaultProps, update selected if (_arraysAreDifferent(this.props.defaultSelected, nextProps.defaultSelected)){ this.setState({selected: nextProps.defaultSelected.slice(0)}) } }, focus: function(){ this.refs.typeahead.focus(); }, getSelectedTokens: function(){ return this.state.selected; }, // TODO: Support initialized tokens // _renderTokens: function() { var tokenClasses = {}; tokenClasses[this.props.customClasses.token] = !!this.props.customClasses.token; var classList = classNames(tokenClasses); var result = this.state.selected.map(function(selected) { var displayString = Accessor.valueForOption(this.props.displayOption, selected); var value = Accessor.valueForOption(this.props.formInputOption || this.props.displayOption, selected); return ( <Token key={displayString} className={classList} onRemove={this._removeTokenForValue} object={selected} value={value} name={this.props.name}> {displayString} </Token> ); }, this); return result; }, _getOptionsForTypeahead: function() { // return this.props.options without this.selected return this.props.options; }, _onKeyDown: function(event) { // We only care about intercepting backspaces if (event.keyCode === KeyEvent.DOM_VK_BACK_SPACE) { return this._handleBackspace(event); } this.props.onKeyDown(event); }, _handleBackspace: function(event){ // No tokens if (!this.state.selected.length) { return; } // Remove token ONLY when bksp pressed at beginning of line // without a selection var entry = this.refs.typeahead.refs.entry; if (entry.selectionStart == entry.selectionEnd && entry.selectionStart == 0) { this._removeTokenForValue( this.state.selected[this.state.selected.length - 1]); event.preventDefault(); } }, _removeTokenForValue: function(value) { var index = this.state.selected.indexOf(value); if (index == -1) { return; } this.state.selected.splice(index, 1); this.setState({selected: this.state.selected}); this.props.onTokenRemove(value); return; }, _addTokenForValue: function(value) { if (this.state.selected.indexOf(value) != -1) { return; } this.state.selected.push(value); this.setState({selected: this.state.selected}); this.refs.typeahead.setEntryText(""); this.props.onTokenAdd(value); }, render: function() { var classes = {}; classes[this.props.customClasses.typeahead] = !!this.props.customClasses.typeahead; var classList = classNames(classes); var tokenizerClasses = [this.props.defaultClassNames && "typeahead-tokenizer"]; tokenizerClasses[this.props.className] = !!this.props.className; var tokenizerClassList = classNames(tokenizerClasses) return ( <div className={tokenizerClassList}> { this._renderTokens() } <Typeahead ref="typeahead" className={classList} placeholder={this.props.placeholder} disabled={this.props.disabled} inputProps={this.props.inputProps} allowCustomValues={this.props.allowCustomValues} customClasses={this.props.customClasses} options={this._getOptionsForTypeahead()} initialValue={this.props.initialValue} maxVisible={this.props.maxVisible} resultsTruncatedMessage={this.props.resultsTruncatedMessage} onOptionSelected={this._addTokenForValue} onKeyDown={this._onKeyDown} onKeyPress={this.props.onKeyPress} onKeyUp={this.props.onKeyUp} onFocus={this.props.onFocus} onBlur={this.props.onBlur} displayOption={this.props.displayOption} defaultClassNames={this.props.defaultClassNames} filterOption={this.props.filterOption} searchOptions={this.props.searchOptions} showOptionsWhenEmpty={this.props.showOptionsWhenEmpty} /> </div> ); } }); module.exports = TypeaheadTokenizer;