react-typeahead
Version:
React-based typeahead and typeahead-tokenizer
217 lines (198 loc) • 6.97 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({
displayName: 'TypeaheadTokenizer',
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 React.createElement(
Token,
{ key: displayString, className: classList,
onRemove: this._removeTokenForValue,
object: selected,
value: value,
name: this.props.name },
displayString
);
}, 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 React.createElement(
'div',
{ className: tokenizerClassList },
this._renderTokens(),
React.createElement(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 })
);
}
});
module.exports = TypeaheadTokenizer;