react-typeahead
Version:
React-based typeahead and typeahead-tokenizer
225 lines (206 loc) • 6.96 kB
JavaScript
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;