zent
Version:
一套前端设计语言和基于React的实现
433 lines (396 loc) • 11.5 kB
JavaScript
/**
* Select
*/
import React, { Component, PureComponent, Children } from 'react';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import isArray from 'lodash/isArray';
import noop from 'lodash/noop';
import cloneDeep from 'lodash/cloneDeep';
import PropTypes from 'prop-types';
import Popover from 'popover';
import Trigger from './trigger';
import Popup from './Popup';
import SimpleTrigger from './trigger/SimpleTrigger';
import SelectTrigger from './trigger/SelectTrigger';
import InputTrigger from './trigger/InputTrigger';
import TagsTrigger from './trigger/TagsTrigger';
class PopoverClickTrigger extends Popover.Trigger.Click {
getTriggerProps(child) {
return {
onClick: evt => {
evt.preventDefault();
if (this.props.contentVisible) {
this.props.close();
} else if (!child.props.disabled) {
this.props.open();
this.triggerEvent(child, 'onClick', evt);
}
}
};
}
}
class Select extends (PureComponent || Component) {
constructor(props) {
super(props);
if (props.simple) {
this.trigger = SimpleTrigger;
} else if (props.search) {
this.trigger = InputTrigger;
} else if (props.tags) {
this.trigger = TagsTrigger;
} else {
this.trigger = props.trigger;
}
this.state = Object.assign(
{
selectedItems: [],
selectedItem: {
value: '',
text: ''
}
},
props
);
}
componentWillMount() {
/**
* data支持字符串数组和对象数组两种模式
*
* 字符串数组默认value为下标
* 对象数组需提供value和text, 或者通过 optionValue-prop optionText-prop 自定义
*
*/
this.uniformedData = this.uniformData(this.props);
this.traverseData(this.props);
}
componentWillReceiveProps(nextProps) {
this.uniformedData = this.uniformData(nextProps);
this.traverseData(nextProps);
}
/**
* 将使用 child-element 传入的 Options 格式化为对象数组(未严格约束)
* data-prop 的优先级高于 child-element
*
* @param {object} props - props of Select
* @returns {object[]} uniformedData - 格式化后对象数组
* @returns {string} uniformedData[].cid - internal id of option
* @returns {string} uniformedData[].text - text of an option
* @returns {any} uniformedData[].value - token of an option
* @memberof Select
*/
uniformData(props) {
const { data, children, optionValue, optionText } = props;
let uniformedData;
// data-prop 高优先级, 格式化 optionValue、optionText
if (data) {
return (uniformedData = data.map((option, index) => {
// 处理字符串数组
if (typeof option !== 'object') {
return { text: option, value: option, cid: `${index}` };
}
// hacky the quirk when optionText = 'value' and avoid modify props
const optCopy = cloneDeep(option);
optCopy.cid = `${index}`;
if (optionValue) {
optCopy.value = option[optionValue];
}
if (optionText) {
optCopy.text = option[optionText];
}
return optCopy;
}));
}
// 格式化 child-element
if (children) {
uniformedData = Children.map(children, (item, index) => {
let value = item.props.value;
value = typeof value === 'undefined' ? item : value;
return Object.assign({}, item.props, {
value,
cid: `${index}`,
text: item.props.children
});
});
}
return uniformedData;
}
/**
* accept uniformed data to traverse then inject selected option or options to next state
*
* @param {object[]} data - uniformedData
* @param {object} props - props of Select
* @memberof Select
*/
traverseData(props, data = this.uniformedData) {
// option 数组置空后重置组件状态
if (!data.length) {
return this.setState({
selectedItem: {},
selectedItems: []
});
}
const { selectedItem, selectedItems } = this.state;
const { value, index, initialIndex, initialValue } = props;
// initialize selected internal state
const selected = { sItem: selectedItem, sItems: [] };
data.forEach((item, i) => {
// 处理 quirk 默认选项(initialIndex, initialValue)
if (
selectedItems.length === 0 &&
!selectedItem.cid &&
(initialValue !== null || initialIndex !== null)
) {
const coord = { value: initialValue, index: initialIndex };
this.locateSelected(selected, coord, item, i);
}
// 处理受控逻辑(index, value)
if (value !== null || index !== null) {
this.locateSelected(selected, { value, index }, item, i);
}
});
this.setState({
selectedItem: selected.sItem,
selectedItems: selected.sItems
});
}
/**
* judge if param 'item' selected
*
* @param {object} state - next state marked selected item or items
* @param {object} coord - coordinate for seleted judging
* @param {object} item - option object after uniformed
* @param {number} i - index of option in options list
* @memberof Select
* */
locateSelected(state, coord, item, i) {
const { value, index } = coord;
if (isArray(value) && value.indexOf(item.value) > -1) {
// rerender 去重
if (!state.sItems.find(selected => selected.value === item.value)) {
state.sItems.push(item);
}
} else if (isArray(value) && value.length === 0) {
// 多选重置
state.sItem = {};
state.sItems = [];
} else if (typeof value === 'object' && isEqual(value, item.value)) {
state.sItem = item;
} else if (
(typeof value !== 'undefined' &&
typeof value !== 'object' &&
`${item.value}` === `${value}`) ||
(index !== 'undefined' && `${i}` === `${index}`)
) {
state.sItem = item;
} else if (!value && !index && value !== 0) {
// github#406 修复option-value为假值数字0时的异常重置。
// 单选重置
state.sItem = {};
state.sItems = [];
}
}
// 接收trigger改变后的数据,将数据传给popup
triggerChangeHandler = data => {
this.setState(data);
};
triggerDeleteHandler = data => {
let { selectedItems } = this.state;
selectedItems = selectedItems.filter(item => item.cid !== data.cid);
this.setState(
{
selectedItems
},
() => {
this.props.onDelete(data);
}
);
};
// 将被选中的option的数据传给trigger
optionChangedHandler = (ev, selectedItem) => {
const result = {};
ev = ev || {
preventDefault: noop,
stopPropagation: noop
};
const {
onEmptySelected,
optionValue,
optionText,
tags,
onChange
} = this.props;
const { selectedItems } = this.state;
if (!selectedItem) {
onEmptySelected(ev);
return;
}
const args = omit(selectedItem, ['cid']);
result[optionValue] = selectedItem.value;
result[optionText] = selectedItem.text;
const data = { ...args, ...result };
if (tags) {
if (!selectedItems.some(item => item.cid === selectedItem.cid)) {
selectedItems.push(selectedItem);
}
}
this.setState(
{
keyword: null,
selectedItems,
selectedItem
},
() => {
onChange(
{
target: {
...this.props,
type: tags ? 'select-multiple' : 'select-one',
value: selectedItem.value
},
preventDefault() {
ev.preventDefault();
},
stopPropagation() {
ev.stopPropagation();
}
},
data
);
}
);
};
handlePopoverVisibleChange = visible => {
if (visible) {
this.props.onOpen();
}
this.setState({ open: visible });
};
render() {
const {
placeholder,
maxToShow,
className,
popupClassName,
disabled,
emptyText,
filter = this.props.onFilter,
onAsyncFilter,
searchPlaceholder,
autoWidth,
width
} = this.props;
const {
open,
selectedItems,
selectedItem = {},
extraFilter,
keyword = null
} = this.state;
const { cid = '' } = selectedItem;
const disabledCls = disabled ? 'disabled' : '';
const prefixCls = `${this.props.prefix}-select`;
return (
<Popover
display="inline-block"
position={Popover.Position.AutoBottomLeft}
visible={open}
className={`${prefixCls} ${popupClassName}`}
wrapperClassName={`${prefixCls} ${className} ${disabledCls}`}
onVisibleChange={this.handlePopoverVisibleChange}
width={width}
>
<PopoverClickTrigger>
<Trigger
disabled={disabled}
prefixCls={prefixCls}
trigger={this.trigger}
placeholder={placeholder}
selectedItems={selectedItems}
keyword={keyword}
{...selectedItem}
onChange={this.triggerChangeHandler}
onDelete={this.triggerDeleteHandler}
/>
</PopoverClickTrigger>
<Popover.Content>
<Popup
ref={ref => (this.popup = ref)}
cid={cid}
prefixCls={prefixCls}
data={this.uniformedData}
selectedItems={selectedItems}
extraFilter={extraFilter}
searchPlaceholder={searchPlaceholder}
emptyText={emptyText}
keyword={keyword}
filter={filter}
onAsyncFilter={onAsyncFilter}
maxToShow={maxToShow}
onChange={this.optionChangedHandler}
onFocus={this.popupFocusHandler}
onBlur={this.popupBlurHandler}
autoWidth={autoWidth}
/>
</Popover.Content>
</Popover>
);
}
}
Select.propTypes = {
data: PropTypes.array,
prefix: PropTypes.string,
className: PropTypes.string,
open: PropTypes.bool,
popupClassName: PropTypes.string,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
maxToShow: PropTypes.number,
searchPlaceholder: PropTypes.string,
emptyText: PropTypes.node,
selectedItem: PropTypes.shape({
value: PropTypes.any,
text: PropTypes.string
}),
trigger: PropTypes.func,
optionValue: PropTypes.string,
optionText: PropTypes.string,
onChange: PropTypes.func,
onDelete: PropTypes.func,
filter: PropTypes.func,
onAsyncFilter: PropTypes.func,
onEmptySelected: PropTypes.func,
onOpen: PropTypes.func,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// 自动根据ref计算弹层宽度
autoWidth: PropTypes.bool
};
Select.defaultProps = {
prefix: 'zent',
disabled: false,
className: '',
open: false,
popupClassName: '',
trigger: SelectTrigger,
placeholder: '请选择',
searchPlaceholder: '',
emptyText: '没有找到匹配项',
selectedItem: {
value: '',
text: ''
},
selectedItems: [],
optionValue: 'value',
optionText: 'text',
onChange: noop,
onDelete: noop,
onEmptySelected: noop,
onOpen: noop,
autoWidth: false,
// HACK
value: null,
index: null,
initialValue: null,
initialIndex: null
};
export default Select;