UNPKG

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
'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