react-taginput
Version:
it's a react component for input multi tags while showing autocompleted dropdown list
237 lines (217 loc) • 5.85 kB
JavaScript
/**
* TODO
* 1. need to add test
* 2. click outside to close dropdown , not use input's blur
*
*/
import React, { Component, PropTypes } from 'react'
import classNames from 'classnames'
// import { uuid } from '../../tool/utils'
// action creators
function startInput () {
return {
// isDropdownOpen: true,
isInput: true,
}
}
function blurInput () {
return {
inputValue: null,
highlightIndex: 0,
// isInput: false,
}
}
function doingInput (val) {
return {
inputValue: val,
highlightIndex: 0,
}
}
function addTag (val, state) {
let value = !!val.trim() ? [...state.value , val.trim() ] : [...state.value]
return {
value,
inputValue: null,
isInput: false,
}
}
function delTag (index, state) {
let value = [...state.value]
value.splice(index,1)
return {
value,
isInput: false,
}
}
function moveHighlight (index, state) {
return {
highlightIndex: index
}
}
// is targets match key or true
function isMatchedWithKey(target, key){
if ( !!key ) {
let keyMatched = target.match(key);
return keyMatched && keyMatched.length
} else {
return true
}
}
// get subset from target subscribed arr and keyword
function getSubset( target, {sub, key=''} ){
return target.filter((item, index)=>{
return sub.indexOf( item ) == -1 && isMatchedWithKey( item, key )
})
}
class TagInput extends Component{
constructor(props){
super(props)
let value = props.valueFormater(props.value)
this.state = {
value,
highlightIndex: 0,
// isDropdownOpen: false,
// isInput == isDropdownOpen
isInput: false,
inputValue: null,
}
// binding
this.handleInputFocus = this.handleInputFocus.bind(this)
this.handleInputBlur = this.handleInputBlur.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
this.handleInputKeyDown = this.handleInputKeyDown.bind(this)
this.handleDel = this.handleDel.bind(this)
this.handleDropdownMouseEnter = this.handleDropdownMouseEnter.bind(this)
this.handleDropdownMouseClick = this.handleDropdownMouseClick.bind(this)
}
componentWillReceiveProps(nextProps){
let value = nextProps.valueFormater(nextProps.value)
this.setState({
value,
// dropdownList: getSubset( nextProps.autoComplete, value )
})
}
getDropdownList(){
let { value, inputValue } = this.state
let targetArr = !!inputValue ? [ inputValue, ...this.props.autoComplete ] : this.props.autoComplete
return getSubset( targetArr, { sub: value, key: inputValue } )
}
handleInputFocus(){
this.setState(startInput() )
}
handleInputBlur(){
this.setState(blurInput() )
}
handleInputChange(e){
this.setState(doingInput(e.target.value) )
}
handleInputKeyDown(e){
let {value, highlightIndex } = this.state;
let dropdownListLength = this.getDropdownList().length;
let valueLength = value.length;
let newState;
switch(e.key){
case 'Enter':
this.setState( addTag(e.target.value, this.state) )
break;
case 'Backspace':
!e.target.value && valueLength && this.setState( delTag(valueLength - 1, this.state) )
break;
case 'ArrowUp':
if( dropdownListLength ){
newState = moveHighlight( highlightIndex ? highlightIndex - 1 : dropdownListLength - 1 , this.state)
this.setState(newState)
}
break;
case 'ArrowDown':
if( dropdownListLength ){
newState = moveHighlight( highlightIndex == dropdownListLength - 1 ? 0 : highlightIndex + 1, this.state)
this.setState(newState)
}
break;
}
}
handleDel(index){
this.setState( delTag( index, this.state ) )
}
handleDropdownMouseEnter(e){
this.setState( moveHighlight( parseInt(e.target.dataset.index) ) )
}
handleDropdownMouseClick(e){
// Problem here
// this click will trigger input blur first,
// then dropdown close , and cannot trigger this handler
this.setState( addTag( e.target.textContent, this.state ) )
}
renderTagList(){
let list = this.state.value.map((tag, index)=>{
return (
<li key={index} className="list-item">
<span className="label label-default pull-left">
{tag}
<i className="fa fa-close" onClick={this.handleDel.bind( this, index )}></i>
</span>
</li>
)
})
return <ul className="tag-list list">{list}</ul>
}
renderInputField(){
// <li className="list-item" key="input-li">{inputDOM}</li>
return <input type="text"
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
onChange={this.handleInputChange}
onKeyDown={this.handleInputKeyDown}
value = {this.state.inputValue} />
}
renderDropdown(){
let { highlightIndex, isInput, value } = this.state
let dropdownList = this.getDropdownList()
let listDOM = dropdownList.map((item, index)=>{
let className = classNames( 'list-item', { 'list-item-highlight': highlightIndex == index } )
// dropdown list is consist of inputValue and autoComplete list
// so index here need to minus 1
// data-index here for move highlight when is triggered mouseenter event
return (
<li className={className}
data-index={index}
key={index}
onMouseEnter={this.handleDropdownMouseEnter}
onClick={this.handleDropdownMouseClick}
>{item}</li>
)
})
console.log('renderDropdown', isInput)
return isInput ? (
<div className="dropdown">
<ul className="auto-complete-list">
{listDOM}
</ul>
</div>
) : null
}
render(){
return(
<div className="c-tag-input" >
<div> {this.renderTagList()} {this.renderInputField()} </div>
{this.renderDropdown()}
</div>
)
}
}
TagInput.propTypes = {
value: PropTypes.array.isRequired,
valueFormater: PropTypes.func,
autoComplete: PropTypes.array,
max: PropTypes.number,
onlyQnique: PropTypes.bool,
}
TagInput.defaultProps = {
value: [],
valueFormater: (val) => val,
autoComplete: [],
max: -1,
onlyQnique: true,
}
export default TagInput