react-tag-input-custom-search
Version:
React Tag Autocomplete is a simple tagging component ready to drop in your React projects.
459 lines (399 loc) • 13.2 kB
JavaScript
'use strict'
const React = require('react')
const PropTypes = require('prop-types')
const Tag = require('./Tag')
const Input = require('./Input')
const Suggestions = require('./Suggestions')
const _ = require('lodash')
const moment = require('moment')
const KEYS = {
ENTER: 13,
TAB: 9,
BACKSPACE: 8,
UP_ARROW: 38,
DOWN_ARROW: 40
}
const 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'
}
class ReactTags extends React.Component {
constructor (props) {
super(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: () => {},
onBlur: this.handleBlur.bind(this),
onFocus: this.handleFocus.bind(this),
onInput: this.handleInput.bind(this),
onKeyDown: this.handleKeyDown.bind(this)
}
}
componentWillReceiveProps (newProps) {
this.setState({
classNames: Object.assign({}, CLASS_NAMES, newProps.classNames)
})
}
handleInput (e) {
let that = this;
let 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)){
let 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 : ''
});
const query = e.target.value.trimLeft();
if(_.get(this,'props.type','') == 'date' || _.get(this,'props.type','') == 'multi_dates'){
let 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'){
let 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 })
}
}
handlePhoneInput(input){
if(input.trim() == '+1')
return '';
input = input.replace("+1 ",'');
let 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;
}
validatePhoneNumber(val){
let 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, ''));
}
handleDateInput(inputDates){
if(_.get(this,'state.delete_multi_dates')){
let arr = inputDates.split(' and');
if(_.get(arr,[1,'length'],0) == 0){
inputDates = arr[0];
}
}
let multi_date = inputDates.substring(19, inputDates.length);
let 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);
}
validateDate(val){
val = val.replace(/\s/g, '');
return moment(val, 'MM/DD/YYYY',true).isValid();
}
validateMultiDateInput(val){
let dates = val.split(' and ');
if(dates.length < 2){
return false;
}
else {
return this.validateDate(dates[0]) && this.validateDate(dates[1])
}
}
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;
};
handleKeyDown (e) {
const { query, selectedIndex } = this.state
const { delimiters, delimiterChars } = this.props
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 })
}
}
handleDelimiter () {
const { query, selectedIndex } = this.state
if (query.length >= this.props.minQueryLength) {
// Check if the user typed in an existing suggestion.
const match = this.suggestions.state.options.findIndex((suggestion) => {
return suggestion.name.search(new RegExp(`^${query}$`, 'i')) === 0
})
const index = selectedIndex === -1 ? match : selectedIndex
if (index > -1) {
this.addTag(this.suggestions.state.options[index])
} else if (this.props.allowNew) {
this.addTag({ name: query })
}
}
}
handleClick (e) {
if (document.activeElement !== e.target) {
this.input.input.focus()
}
}
handleBlur () {
this.setState({ focused: false, selectedIndex: -1 })
if (this.props.handleBlur) {
this.props.handleBlur()
}
if (this.props.addOnBlur) {
this.handleDelimiter()
}
}
handleFocus () {
this.setState({ focused: true })
if (this.props.handleFocus) {
this.props.handleFocus()
}
}
addTag (tag) {
let 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;
}
const {tags, allowUnique} = this.props;
if (typeof this.props.handleValidate === 'function' && !this.props.handleValidate(tag)) {
return
}
const existingKeys = tags.map((tag) => tag.id);
if (allowUnique && existingKeys.indexOf(tag.id) >= 0) {
return;
}
this.props.handleAddition(tag)
// reset the state
this.setState({
query: '',
selectedIndex: -1
})
}
deleteTag (i) {
this.props.handleDelete(i)
if (this.props.clearInputOnDelete && this.state.query !== '') {
this.setState({ query: '' })
}
}
render () {
const listboxId = 'ReactTags-listbox'
const TagComponent = this.props.tagComponent || Tag
const tags = this.props.tags.map((tag, i) => (
<TagComponent
key={i}
tag={tag}
classNames={this.state.classNames}
onDelete={this.deleteTag.bind(this, i)} />
))
const expandable = this.state.focused && this.state.query.length >= this.props.minQueryLength
const classNames = [this.state.classNames.root]
this.state.focused && classNames.push(this.state.classNames.rootFocused)
return (
<div className={classNames.join(' ')} onClick={this.handleClick.bind(this)}>
<div className={this.state.classNames.selected} aria-live='polite' aria-relevant='additions removals'>
{tags}
</div>
<div className={this.state.classNames.search}>
<Input {...this.state}
inputAttributes={this.props.inputAttributes}
inputEventHandlers={this.inputEventHandlers}
ref={(c) => { this.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} />
<Suggestions {...this.state}
ref={(c) => { this.suggestions = c }}
listboxId={listboxId}
expandable={expandable}
suggestions={this.props.suggestions}
suggestionsFilter={this.props.suggestionsFilter}
addTag={this.addTag.bind(this)}
maxSuggestionsLength={this.props.maxSuggestionsLength} />
</div>
</div>
)
}
}
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