UNPKG

react-ui-component

Version:
667 lines (607 loc) 20.2 kB
const React = require('react') const Component = React.Component const PropTypes = require('prop-types') const ReactDOM = require('react-dom') const defaultCheckedCmp = require('./high-order/defaultCheckedCmp') const documentClickCmp = require('./high-order/documentClickCmp') const dropDownCmp = require('./high-order/dropDownCmp') const BACKSPACE_KEYCODE = require('./util/constants').BACKSPACE_KEYCODE const klassName = require('./util/className') const debounce = require('./util/debounce') class DropDown extends Component { constructor(props) { super(props); this.toggleOpen = this.toggleOpen.bind(this) this.multiBarValChangeByIndex = this.multiBarValChangeByIndex.bind(this) this.handleChangeSelect = this.handleChangeSelect.bind(this) this.handleSearch = this.handleSearch.bind(this) let { multi, value } = props; const default_val = multi ? [] : ''; value = [undefined, null, ''].indexOf(value) === -1 ? value : default_val; this.state = { value, open: false, filterText: '', } } componentWillReceiveProps(nextProps) { const { defaultSelected, multi } = this.props this.optionsChangeReInitValue({ defaultChecked: defaultSelected, nextProps, multi }) } componentDidMount() { let {value} = this.state let {defaultSelected, multi} = this.props if (defaultSelected) { if (!multi && !value) { return this.initDefaultCheckedValue({ multi }) } if (multi && value.length === 0) { this.initDefaultCheckedValue({ multi }) } } } // format value judge if is multi formatValue(val, callback){ let newVal = val, oldVal = this.state.value; if (this.props.multi) { let index = oldVal.indexOf(val); if (index > -1) { oldVal.splice(index, 1); this.setState({ value: oldVal }, callback); return; } newVal = oldVal.concat(val); } this.setState({ value: newVal}, callback); } // yield child type dropdown formatYieldChildren(children){ let {labelName, valueName, placeHolder, multi, style, className} = this.props; const {filterText, value, open} = this.state; let nodes = [], tags = []; React.Children.map(children, (item, index) => { const props = item.props; const item_val = props[valueName]; const item_label = props[labelName]; let selected = false; if (multi) { const _index = value.indexOf(item_val); selected = _index !== -1; if(selected) tags[_index] = item_label; } else { selected = value === item_val; if (selected) { placeHolder = item_label; } } if(this.filterTextMatched(filterText, item_label, item_val)) { nodes.push( <DropDownOption key={index} value={item_val} label={item_label} disabled={props.disabled} onClick={() => this.handleChangeSelect(item_val)} selected={selected}> {props.children} </DropDownOption> ) } }); let labelNode = this.formatLabel(multi ? tags : placeHolder) className = klassName('dropdown', className); if (open) { className = `${className} _active`; } return <div className={className} style={style}> {labelNode} <span className="_list"> {open ? nodes : null} {this.formatLoading()} </span> </div>; } // add loading to list formatLoading(){ const { loading } = this.props const { open } = this.state if (!open) { return null } return ( loading ? <div className="_overlay"> <div className="loader"> </div> </div> : null ) } // dropdown label formatLabel(labels){ const { multi, searchable } = this.props; let labelNode = null; if (multi) { labelNode = this.formatMultiInput(labels) } else { labelNode = searchable ? this.formatSearchBar(labels) : this.formatDefaultLabel(labels) } return labelNode; } // generate nodes and label by options prop getNodesAndLabel(){ const { labelName, valueName, searchable, multi, placeHolder, options } = this.props; const { filterText, value } = this.state; let displayLabels = [], node = null, optionNodes = []; if (!multi) displayLabels = placeHolder; for (let i = 0; i < options.length; i++) { const pair = options[i]; const pair_val = pair[valueName]; const pair_label = pair[labelName]; let selected = false; if (multi) { for (let j = 0; j < value.length; j++) { selected = value[j] === pair_val; if (selected) { if (displayLabels.indexOf(pair_label) === -1) displayLabels[value.indexOf(pair_val)] = pair_label; break; } } } else { selected = value === pair_val; if (selected) displayLabels = pair_label; } node = <DropDownOption key={i} label={pair_label} value={pair_val} selected={selected} onClick={() => this.handleChangeSelect(pair_val)} disabled={pair.disabled}> {pair.children} </DropDownOption> if (multi || searchable) { if (this.filterTextMatched(filterText, pair_val, pair_label)) optionNodes.push(node); } else { optionNodes.push(node); } } return {optionNodes, displayLabels}; } // options type dropdown formatOptions(){ let {className, style} = this.props; const {open} = this.state className = klassName('dropdown', className); if (open) { className += ' _active'; } const {optionNodes, displayLabels} = this.getNodesAndLabel(); return ( <div className={className} style={style}> {this.formatLabel(displayLabels)} <span className="_list"> {open ? optionNodes : null} {this.formatLoading()} </span> </div> ); } // filter if text in fields, return true filterTextMatched(text, ...fields){ let status = false; for (let i = 0; i < fields.length; i++) { if (String(fields[i]).indexOf(text) !== -1) { status = true; break; } } return status; } // default label for dropdown formatDefaultLabel(text) { const { disabled, onFocus, onBlur, onClick } = this.props const { value, open } = this.state return ( disabled ? <DropDownLabel disabled> {text} </DropDownLabel> : <DropDownLabel onFocus={onFocus} onBlur={onBlur} isPlaceHolder={value === ''} onClick={ () => { this.toggleOpen(!open) if (onClick) onClick() } }> {text} </DropDownLabel> ) } // searchable search bar formatSearchBar(labels){ const { filterText, value } = this.state; const { disabled, onClick, onBlur, onFocus } = this.props return ( disabled ? <DropDownSearchBar placeHolder={labels} text={filterText} isPlaceHolder={value === ''} disabled={disabled} onUserInput={() => void 0 } /> : <DropDownSearchBar placeHolder={labels} text={filterText} isPlaceHolder={value === ''} disabled={disabled} onClick={ () => { this.toggleOpen(true) if (onClick) onClick() } } onBlur={onBlur} onFocus={onFocus} onUserInput={this.handleSearch} /> ) } // multi dropdown's input formatMultiInput(tags){ const { onClick, onBlur, onFocus, disabled } = this.props return ( disabled ? <MultiInput disabled={disabled} filterText={this.state.filterText} onClick={() => void 0 } selectedTags={tags} /> : <MultiInput disabled={disabled} filterText={this.state.filterText} onSelectChange={this.multiBarValChangeByIndex} onUserInputFocus={ () => { this.toggleOpen(true) if (onFocus) onFocus() } } onBlur={onBlur} onFocus={onFocus} onUserInput={this.handleSearch} onClick={ () => { this.toggleOpen(true) if (onClick) onClick() } } selectedTags={tags} /> ); } // other context click to close dropdown onOtherDomClick(){ this.toggleOpen(false); } // multi dropdown, select value change multiBarValChangeByIndex(index){ let {value} = this.state; // remove specific value if (index !== undefined) { if (index > -1) value.splice(index, 1); } else { value.pop(); } this.setState({ value, }, this.triggerDropValueChange()); } // each option cell clicked handler handleChangeSelect(val){ this.formatValue(val, () => { this.triggerDropValueChange(); this.toggleOpen(this.props.multi); }); } // value change, trigger onChange event triggerDropValueChange(){ const {multi, onChange} = this.props; let {value} = this.state; if (multi) value = Object.assign([], value); onChange(value); } // dropdown open or close, clean filter text toggleOpen(stat){ let params = { open: stat, } if (this.state.value || this.props.autoClearText) { params.filterText = '' } this.setState(params); } clearText(){ this.setState({ filterText: '' }); } // search handleSearch(text){ this.setState({ filterText: text, }); const {onSearch} = this.props if (onSearch) { debounce(onSearch)(text) } } render() { const {children} = this.props; let node = children ? this.formatYieldChildren(children) : this.formatOptions(); return ( node ); } } DropDown.propTypes = { placeHolder: PropTypes.string, options: PropTypes.array, autoClearText: PropTypes.bool, onChange: PropTypes.func.isRequired, onClick: PropTypes.func, onSearch: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, labelName: PropTypes.string, valueName: PropTypes.string, defaultSelected: PropTypes.bool, searchable: PropTypes.bool, multi: PropTypes.bool, disabled: PropTypes.bool, children: PropTypes.arrayOf(PropTypes.element), } DropDown.defaultProps = { labelName: 'name', valueName: 'value', autoClearText: false, multi: false, disabled: false, className: '', placeHolder: 'click to select...', } const DropDownOption = props => { let newProps = Object.assign({}, props) const {selected, disabled, children} = newProps; let className = '_item'; if (disabled) { className += ' _disabled'; delete newProps.onClick } if (selected) { className += ' _active'; } return ( <div className={className} {...newProps}> {children ? children : newProps.label} </div> ) } // dropdown label const DropDownLabel = props => { let _props = Object.assign({}, props) const { isPlaceHolder, disabled } = _props const className = klassName( '_label', isPlaceHolder ? '_placeHolder' : '', disabled ? '_disabled' : '', ) delete _props.onClick delete _props.disabled delete _props.onBlur delete _props.onFocus delete _props.isPlaceHolder return ( <div {..._props} className={className} onClick={props.onClick}> <input type="text" className="_transparent" readOnly disabled={disabled} onBlur={props.onBlur} onFocus={props.onFocus}/> <i></i> <div className="_text"> {_props.children} </div> </div> ); } const DropDownSearchBar = props => { const { onClick, onBlur, onFocus, text, isPlaceHolder, placeHolder, disabled } = props const className = klassName( '_text', isPlaceHolder ? '_placeHolder' : '', ) return ( <div className={ klassName( '_search', disabled ? '_disabled' : '' ) } onClick={onClick}> { text ? <div className="_text"></div> : <div className={className}> {placeHolder} </div> } <input type='text' className='_input' value={text} disabled={disabled} onBlur={onBlur} onFocus={onFocus} onChange={e => props.onUserInput(e.target.value)} /> <i></i> </div> ) } DropDownSearchBar.propTypes = { onClick: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, text: PropTypes.string, placeHolder: PropTypes.string, isPlaceHolder: PropTypes.bool, onUserInput: PropTypes.func.isRequired, } DropDownSearchBar.defaultProps = { text: '', placeHolder: '', isPlaceHolder: false, } // multi dropdown input field class MultiInput extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) this.handleBlur = this.handleBlur.bind(this) this.removeSelected = this.removeSelected.bind(this) this.state = { hasInput: false, } } componentWillReceiveProps(nextProps) { if (nextProps.selectedTags.length !== this.props.selectedTags.length) { this.inputFieldFocus(); } } handleClick(){ this.inputFieldFocus(); this.props.onClick(true); } handleKeyDown(e){ const {keyCode} = e; const value = this.inputField().value; this.setState({ hasInput: true, }); if (keyCode === BACKSPACE_KEYCODE && value === '') this.props.onSelectChange(); e.target.style.width = (value.length + 1) * 12 + 'px'; } handleBlur(){ const {onBlur} = this.props this.setState({ hasInput: false, }); this.inputField().style.width = '9px'; if (onBlur) onBlur() } removeSelected(index){ this.props.onSelectChange(index); this.inputFieldFocus(); } inputField(){ return ReactDOM.findDOMNode(this.userInput); } inputFieldFocus(){ this.inputField().focus(); } render() { const { selectedTags, filterText, disabled } = this.props; const { hasInput } = this.state; const tagNodes = selectedTags.map((tag, index) => { return ( disabled ? <span className='_tag _disabled' key={index}> <span className="_text"> {tag} </span> <a href="javascript:;" className="_delete"> </a> </span> : <span className='_tag' key={index} onClick={() => this.removeSelected(index)}> <span className="_text"> {tag} </span> <a href="javascript:;" className="_delete"> </a> </span> ) }); let placeHolder = selectedTags.length === 0 && !hasInput ? <span className='_placeHolder'>search...</span> : <span className='_placeHolder'></span>; return ( <div className={ klassName( '_multi', disabled ? '_disabled' : '' ) } onClick={this.handleClick}> {tagNodes} { disabled ? <input type="text" className="_input" ref={ ref => { this.userInput = ref } } style={{ 'width': '9px' }} value={ filterText } disabled /> : <input type="text" className="_input" ref={ ref => { this.userInput = ref } } style={{'width': '9px'}} value={filterText} onBlur={this.handleBlur} onFocus={ e => this.props.onUserInputFocus(e)} onChange={ e => this.props.onUserInput(e.target.value) } onKeyDown={this.handleKeyDown} /> } {placeHolder} <i></i> </div> ); } } MultiInput.propTypes = { onUserInputFocus: PropTypes.func, onUserInput: PropTypes.func, filterText: PropTypes.string, disabled: PropTypes.bool, } module.exports = dropDownCmp(documentClickCmp(defaultCheckedCmp(DropDown)), ['top', 'bottom'])