react-tag-input-custom-search
Version:
React Tag Autocomplete is a simple tagging component ready to drop in your React projects.
455 lines (392 loc) • 14.4 kB
JavaScript
'use strict'
var React = require('react')
var PropTypes = require('prop-types')
var Tag = require('./Tag')
var Input = require('./Input')
var Suggestions = require('./Suggestions')
var _ = require('lodash')
var moment = require('moment')
var KEYS = {
ENTER: 13,
TAB: 9,
BACKSPACE: 8,
UP_ARROW: 38,
DOWN_ARROW: 40
}
var CLASS_NAMES = {
root: 'react-tags',
rootFocused: 'is-focused',
selected: 'react-tags__selected',
selectedTag: 'react-tags__selected-tag',
selectedTagName: 'react-tags__selected-tag-name',
search: 'react-tags__search',
searchInput: 'react-tags__search-input',
suggestions: 'react-tags__suggestions',
suggestionActive: 'is-active',
suggestionDisabled: 'is-disabled'
}
var ReactTags = (function (superclass) {
function ReactTags (props) {
superclass.call(this, props)
this.state = {
query: '',
focused: false,
expandable: false,
selectedIndex: -1,
classNames: Object.assign({}, CLASS_NAMES, this.props.classNames)
}
this.inputEventHandlers = {
// Provide a no-op function to the input component to avoid warnings
// <https://github.com/i-like-robots/react-tags/issues/135>
// <https://github.com/facebook/react/issues/13835>
onChange: function () {},
onBlur: this.handleBlur.bind(this),
onFocus: this.handleFocus.bind(this),
onInput: this.handleInput.bind(this),
onKeyDown: this.handleKeyDown.bind(this)
}
}
if ( superclass ) ReactTags.__proto__ = superclass;
ReactTags.prototype = Object.create( superclass && superclass.prototype );
ReactTags.prototype.constructor = ReactTags;
ReactTags.prototype.componentWillReceiveProps = function componentWillReceiveProps (newProps) {
this.setState({
classNames: Object.assign({}, CLASS_NAMES, newProps.classNames)
})
};
ReactTags.prototype.handleInput = function handleInput (e) {
var that = this;
var input_str = e.target.value || '';
if(_.get(this,'props.maxQueryLength') && input_str.length > _.get(this,'props.maxQueryLength')){
this.setState({
input_class : 'animate',
query : input_str.substring(0,_.get(this,'props.maxQueryLength'))
});
setTimeout(function(){
that.setState({
input_class : '',
})
}, 1000);
return;
}
if(_.get(this,'props.restrictInput',false)){
var is_substring_suggestion = _.get(this,'props.suggestions',[]).some(function(v) {
return _.get(v,'name','').toLowerCase().indexOf(input_str.toLowerCase()) == 0
});
if(!is_substring_suggestion){
if (this.props.handleInputChange) {
this.props.handleInputChange(input_str.substring(0,input_str.length-1))
}
this.setState({
input_class : 'animate',
query : input_str.substring(0,input_str.length-1)
});
setTimeout(function(){
that.setState({
input_class : '',
})
}, 1000);
return;
}
}
this.setState({
input_class : ''
});
var query = e.target.value.trimLeft();
if(_.get(this,'props.type','') == 'date' || _.get(this,'props.type','') == 'multi_dates'){
var date_query = this.handleDateInput(query);
if(date_query.length == 14 && _.get(this,'props.type','') == 'multi_dates' && !_.get(this,'state.delete_multi_dates')){
date_query = date_query + ' and ';
}
if (this.props.handleInputChange) {
this.props.handleInputChange(date_query)
}
this.setState({ query : date_query })
}
else if(_.get(this,'props.type','') == 'phone'){
var phone_query = this.handlePhoneInput(query);
if (this.props.handleInputChange) {
this.props.handleInputChange(phone_query)
}
this.setState({ query : phone_query })
}
else{
if (this.props.handleInputChange) {
this.props.handleInputChange(query)
}
this.setState({ query : query })
}
};
ReactTags.prototype.handlePhoneInput = function handlePhoneInput (input){
if(input.trim() == '+1')
{ return ''; }
input = input.replace("+1 ",'');
var x = input.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
input = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
return "+1 " + input;
};
ReactTags.prototype.validatePhoneNumber = function validatePhoneNumber (val){
var valid_phone_reg = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s\.]{0,1}[0-9]{3}[-\s\.]{0,1}[0-9]{4}$/;
return valid_phone_reg.test(val.replace("+1 ",'').replace(/\D/g, ''));
};
ReactTags.prototype.handleDateInput = function handleDateInput (inputDates){
if(_.get(this,'state.delete_multi_dates')){
var arr = inputDates.split(' and');
if(_.get(arr,[1,'length'],0) == 0){
inputDates = arr[0];
}
}
var multi_date = inputDates.substring(19, inputDates.length);
var input = inputDates;
if(inputDates.indexOf(' and ') > -1){
input = multi_date;
}
if (/\D\/$/.test(input)) { input = input.substr(0, input.length - 3); }
var values = input.split('/').map(function(v) {
return v.replace(/\D/g, '')
});
if (values[0]) { values[0] = this.checkValue(values[0], 12); }
if (values[1]) { values[1] = this.checkValue(values[1], 31); }
var output = values.map(function(v, i) {
return v.length == 2 && i < 2 ? v + ' / ' : v;
});
return inputDates.indexOf(' and ') > -1 ? (inputDates.substring(0,19) + output.join('').substr(0, 14)) : output.join('').substr(0, 14);
};
ReactTags.prototype.validateDate = function validateDate (val){
val = val.replace(/\s/g, '');
return moment(val, 'MM/DD/YYYY',true).isValid();
};
ReactTags.prototype.validateMultiDateInput = function validateMultiDateInput (val){
var dates = val.split(' and ');
if(dates.length < 2){
return false;
}
else {
return this.validateDate(dates[0]) && this.validateDate(dates[1])
}
};
ReactTags.prototype.checkValue = function checkValue (str, max) {
if (str.charAt(0) !== '0' || str == '00') {
var num = parseInt(str);
if (isNaN(num) || num <= 0 || num > max) { num = 1; }
str = num > parseInt(max.toString().charAt(0))
&& num.toString().length == 1 ? '0' + num : num.toString();
};
return str;
};;
ReactTags.prototype.handleKeyDown = function handleKeyDown (e) {
var ref = this.state;
var query = ref.query;
var selectedIndex = ref.selectedIndex;
var ref$1 = this.props;
var delimiters = ref$1.delimiters;
var delimiterChars = ref$1.delimiterChars;
if(e.keyCode != KEYS.BACKSPACE){
this.setState({
delete_multi_dates: false
});
}
// when one of the terminating keys is pressed, add current query to the tags.
if (delimiters.indexOf(e.keyCode) > -1 || delimiterChars.indexOf(e.key) > -1) {
if (query || selectedIndex > -1) {
e.preventDefault()
}
this.handleDelimiter()
}
// when backspace key is pressed and query is blank, delete the last tag
if (e.keyCode === KEYS.BACKSPACE && query.length === 0 && this.props.allowBackspace) {
this.deleteTag(this.props.tags.length - 1)
}
if(e.keyCode === KEYS.BACKSPACE && _.get(this,'props.type','') == 'multi_dates'){
this.setState({
delete_multi_dates: true
});
}
if (e.keyCode === KEYS.UP_ARROW) {
e.preventDefault()
// if last item, cycle to the bottom
if (selectedIndex <= 0) {
this.setState({ selectedIndex: this.suggestions.state.options.length - 1 })
} else {
this.setState({ selectedIndex: selectedIndex - 1 })
}
}
if (e.keyCode === KEYS.DOWN_ARROW) {
e.preventDefault()
this.setState({ selectedIndex: (selectedIndex + 1) % this.suggestions.state.options.length })
}
};
ReactTags.prototype.handleDelimiter = function handleDelimiter () {
var ref = this.state;
var query = ref.query;
var selectedIndex = ref.selectedIndex;
if (query.length >= this.props.minQueryLength) {
// Check if the user typed in an existing suggestion.
var match = this.suggestions.state.options.findIndex(function (suggestion) {
return suggestion.name.search(new RegExp(("^" + query + "$"), 'i')) === 0
})
var index = selectedIndex === -1 ? match : selectedIndex
if (index > -1) {
this.addTag(this.suggestions.state.options[index])
} else if (this.props.allowNew) {
this.addTag({ name: query })
}
}
};
ReactTags.prototype.handleClick = function handleClick (e) {
if (document.activeElement !== e.target) {
this.input.input.focus()
}
};
ReactTags.prototype.handleBlur = function handleBlur () {
this.setState({ focused: false, selectedIndex: -1 })
if (this.props.handleBlur) {
this.props.handleBlur()
}
if (this.props.addOnBlur) {
this.handleDelimiter()
}
};
ReactTags.prototype.handleFocus = function handleFocus () {
this.setState({ focused: true })
if (this.props.handleFocus) {
this.props.handleFocus()
}
};
ReactTags.prototype.addTag = function addTag (tag) {
var that = this;
// if(_.get(this,'props.type','') == 'phone' && !this.validatePhoneNumber(tag.name)){
// this.setState({
// input_class : 'animate'
// });
// setTimeout(function(){
// that.setState({
// input_class : ''
// })
// }, 1000);
// return;
// }
if(tag.disabled){
return;
}
// if(_.includes(_.get(this,'props.selected_ids',[]), tag.id)){
// console.log('tag already exists');
// return;
// }
if(_.get(this,'props.type','') == 'date' && !this.validateDate(tag.name)){
this.setState({
input_class : 'animate'
});
setTimeout(function(){
that.setState({
input_class : ''
})
}, 1000);
return;
}
else if(_.get(this,'props.type','') == 'multi_dates' && !this.validateMultiDateInput(tag.name)){
this.setState({
input_class : 'animate'
});
setTimeout( that.setState({
input_class : ''
}),1000 );
return;
}
var ref = this.props;
var tags = ref.tags;
var allowUnique = ref.allowUnique;
if (typeof this.props.handleValidate === 'function' && !this.props.handleValidate(tag)) {
return
}
var existingKeys = tags.map(function (tag) { return tag.id; });
if (allowUnique && existingKeys.indexOf(tag.id) >= 0) {
return;
}
this.props.handleAddition(tag)
// reset the state
this.setState({
query: '',
selectedIndex: -1
})
};
ReactTags.prototype.deleteTag = function deleteTag (i) {
this.props.handleDelete(i)
if (this.props.clearInputOnDelete && this.state.query !== '') {
this.setState({ query: '' })
}
};
ReactTags.prototype.render = function render () {
var this$1 = this;
var listboxId = 'ReactTags-listbox'
var TagComponent = this.props.tagComponent || Tag
var tags = this.props.tags.map(function (tag, i) { return (
React.createElement( TagComponent, {
key: i, tag: tag, classNames: this$1.state.classNames, onDelete: this$1.deleteTag.bind(this$1, i) })
); })
var expandable = this.state.focused && this.state.query.length >= this.props.minQueryLength
var classNames = [this.state.classNames.root]
this.state.focused && classNames.push(this.state.classNames.rootFocused)
return (
React.createElement( 'div', { className: classNames.join(' '), onClick: this.handleClick.bind(this) },
React.createElement( 'div', { className: this.state.classNames.selected, 'aria-live': 'polite', 'aria-relevant': 'additions removals' },
tags
),
React.createElement( 'div', { className: this.state.classNames.search },
React.createElement( Input, Object.assign({}, this.state, { inputAttributes: this.props.inputAttributes, inputEventHandlers: this.inputEventHandlers, ref: function (c) { this$1.input = c }, listboxId: listboxId, autofocus: this.props.autofocus, autoresize: this.props.autoresize, expandable: expandable, type: this.props.type, className: _.get(this,'state.input_class'), placeholder: this.props.placeholder })),
React.createElement( Suggestions, Object.assign({}, this.state, { ref: function (c) { this$1.suggestions = c }, listboxId: listboxId, expandable: expandable, suggestions: this.props.suggestions, suggestionsFilter: this.props.suggestionsFilter, addTag: this.addTag.bind(this), maxSuggestionsLength: this.props.maxSuggestionsLength }))
)
)
)
};
return ReactTags;
}(React.Component));
ReactTags.defaultProps = {
tags: [],
placeholder: 'Add new name',
suggestions: [],
suggestionsFilter: null,
autofocus: true,
autoresize: true,
delimiters: [KEYS.TAB, KEYS.ENTER],
delimiterChars: [],
minQueryLength: 2,
maxSuggestionsLength: 6,
allowNew: false,
allowBackspace: true,
tagComponent: null,
inputAttributes: {},
addOnBlur: false,
clearInputOnDelete: true,
}
ReactTags.propTypes = {
tags: PropTypes.arrayOf(PropTypes.object),
placeholder: PropTypes.string,
suggestions: PropTypes.arrayOf(PropTypes.object),
possibleSuggestions: PropTypes.arrayOf(PropTypes.object),
suggestionsFilter: PropTypes.func,
autofocus: PropTypes.bool,
autoresize: PropTypes.bool,
delimiters: PropTypes.arrayOf(PropTypes.number),
delimiterChars: PropTypes.arrayOf(PropTypes.string),
handleDelete: PropTypes.func.isRequired,
handleAddition: PropTypes.func.isRequired,
handleInputChange: PropTypes.func,
handleFocus: PropTypes.func,
handleBlur: PropTypes.func,
handleValidate: PropTypes.func,
minQueryLength: PropTypes.number,
maxSuggestionsLength: PropTypes.number,
classNames: PropTypes.object,
handleFilterSuggestions: PropTypes.func,
allowNew: PropTypes.bool,
allowBackspace: PropTypes.bool,
tagComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.element
]),
inputAttributes: PropTypes.object,
addOnBlur: PropTypes.bool,
clearInputOnDelete: PropTypes.bool
}
module.exports = ReactTags