UNPKG

react-tag-input

Version:

React tags is a fantastically simple tagging component for your React projects

277 lines (252 loc) 9.31 kB
var React = require('react'); var ReactDOM = require('react-dom'); var Tag = require('./tag'); var Suggestions = require('./suggestions'); var { DragDropContext } = require('react-dnd'); var HTML5Backend = require('react-dnd-html5-backend'); var merge = require('lodash/fp/merge'); // Constants const Keys = { ENTER: 13, TAB: 9, BACKSPACE: 8, UP_ARROW: 38, DOWN_ARROW: 40, ESCAPE: 27 }; const DefaultClassNames = { tags: 'ReactTags__tags', tagInput: 'ReactTags__tagInput', selected: 'ReactTags__selected', tag: 'ReactTags__tag', remove: 'ReactTags__remove', suggestions: 'ReactTags__suggestions' }; var ReactTags = React.createClass({ propTypes: { tags: React.PropTypes.array, placeholder: React.PropTypes.string, labelField: React.PropTypes.string, suggestions: React.PropTypes.array, delimiters: React.PropTypes.array, autofocus: React.PropTypes.bool, inline: React.PropTypes.bool, handleDelete: React.PropTypes.func.isRequired, handleAddition: React.PropTypes.func.isRequired, handleDrag: React.PropTypes.func, allowDeleteFromEmptyInput: React.PropTypes.bool, handleInputChange: React.PropTypes.func, minQueryLength: React.PropTypes.number, shouldRenderSuggestions: React.PropTypes.func, removeComponent: React.PropTypes.func, autocomplete: React.PropTypes.oneOfType([React.PropTypes.bool, React.PropTypes.number]), readOnly: React.PropTypes.bool, classNames: React.PropTypes.object }, getDefaultProps: function() { return { placeholder: 'Add new tag', tags: [], suggestions: [], delimiters: [Keys.ENTER, Keys.TAB], autofocus: true, inline: true, allowDeleteFromEmptyInput: true, minQueryLength: 2, autocomplete: false, readOnly: false, } }, componentWillMount: function() { this.setState({ classNames: merge(DefaultClassNames, this.props.classNames) }); }, componentDidMount: function() { if (this.props.autofocus && !this.props.readOnly) { this.refs.input.focus(); } }, getInitialState: function() { return { suggestions: this.props.suggestions, query: "", selectedIndex: -1, selectionMode: false } }, filteredSuggestions(query, suggestions) { return suggestions.filter(function(item) { return item.toLowerCase().indexOf(query.toLowerCase()) === 0; }); }, componentWillReceiveProps(props) { var suggestions = this.filteredSuggestions(this.state.query, props.suggestions); this.setState({ suggestions: suggestions, classNames: merge(DefaultClassNames, props.classNames) }); }, handleDelete: function(i, e) { this.props.handleDelete(i); this.setState({ query: "" }); }, handleChange: function(e) { if (this.props.handleInputChange){ this.props.handleInputChange(e.target.value.trim()) } var query = e.target.value.trim(); var suggestions = this.filteredSuggestions(query, this.props.suggestions); this.setState({ query: query, suggestions: suggestions }); }, handleKeyDown: function(e) { var { query, selectedIndex, suggestions } = this.state; // hide suggestions menu on escape if (e.keyCode === Keys.ESCAPE) { e.preventDefault(); e.stopPropagation(); this.setState({ selectedIndex: -1, selectionMode: false, suggestions: [] }); } // When one of the terminating keys is pressed, add current query to the tags. // If no text is typed in so far, ignore the action - so we don't end up with a terminating // character typed in. if (this.props.delimiters.indexOf(e.keyCode) !== -1) { if (e.keyCode !== Keys.TAB || query !== "") { e.preventDefault(); } if (query !== "") { if (this.state.selectionMode) { query = this.state.suggestions[this.state.selectedIndex]; } this.addTag(query); } } // when backspace key is pressed and query is blank, delete tag if (e.keyCode === Keys.BACKSPACE && query == "" && this.props.allowDeleteFromEmptyInput) { this.handleDelete(this.props.tags.length - 1); } // up arrow if (e.keyCode === Keys.UP_ARROW) { e.preventDefault(); var selectedIndex = this.state.selectedIndex; // last item, cycle to the top if (selectedIndex <= 0) { this.setState({ selectedIndex: this.state.suggestions.length - 1, selectionMode: true }); } else { this.setState({ selectedIndex: selectedIndex - 1, selectionMode: true }); } } // down arrow if (e.keyCode === Keys.DOWN_ARROW) { e.preventDefault(); this.setState({ selectedIndex: (this.state.selectedIndex + 1) % suggestions.length, selectionMode: true }); } }, addTag: function(tag) { var input = this.refs.input; if (this.props.autocomplete) { var possibleMatches = this.filteredSuggestions(tag, this.props.suggestions); if ( (this.props.autocomplete === 1 && possibleMatches.length === 1) || this.props.autocomplete === true && possibleMatches.length) { tag = possibleMatches[0] } } // call method to add this.props.handleAddition(tag); // reset the state this.setState({ query: "", selectionMode: false, selectedIndex: -1 }); // focus back on the input box input.value = ""; input.focus(); }, handleSuggestionClick: function(i, e) { this.addTag(this.state.suggestions[i]); }, handleSuggestionHover: function(i, e) { this.setState({ selectedIndex: i, selectionMode: true }); }, moveTag: function(id, afterId) { var tags = this.props.tags; // locate tags var tag = tags.filter(t => t.id === id)[0]; var afterTag = tags.filter(t => t.id === afterId)[0]; // find their position in the array var tagIndex = tags.indexOf(tag); var afterTagIndex = tags.indexOf(afterTag); // call handler with current position and after position this.props.handleDrag(tag, tagIndex, afterTagIndex); }, render: function() { var moveTag = this.props.handleDrag?this.moveTag:null; var tagItems = this.props.tags.map(function(tag, i) { return <Tag key={i} tag={tag} labelField={this.props.labelField} onDelete={this.handleDelete.bind(this, i)} moveTag={moveTag} removeComponent={this.props.removeComponent} readOnly={this.props.readOnly} classNames={this.state.classNames}/> }.bind(this)); // get the suggestions for the given query var query = this.state.query.trim(), selectedIndex = this.state.selectedIndex, suggestions = this.state.suggestions, placeholder = this.props.placeholder; const tagInput = !this.props.readOnly ? ( <div className={this.state.classNames.tagInput}> <input ref="input" type="text" placeholder={placeholder} aria-label={placeholder} onChange={this.handleChange} onKeyDown={this.handleKeyDown}/> <Suggestions query={query} suggestions={suggestions} selectedIndex={selectedIndex} handleClick={this.handleSuggestionClick} handleHover={this.handleSuggestionHover} minQueryLength={this.props.minQueryLength} shouldRenderSuggestions={this.props.shouldRenderSuggestions} classNames={this.state.classNames}/> </div> ) : null; return ( <div className={this.state.classNames.tags}> <div className={this.state.classNames.selected}> {tagItems} {this.props.inline && tagInput} </div> {!this.props.inline && tagInput} </div> ) } }); module.exports = { WithContext: DragDropContext(HTML5Backend)(ReactTags), WithOutContext: ReactTags, Keys: Keys };