react-ui-component
Version:
some component build with ReactJs
667 lines (607 loc) • 20.2 kB
JavaScript
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'])