2020-03-31 13:47:59 +08:00

402 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { func, obj, KEYCODE } from '../util';
import Input from '../input';
import Base from './base';
const { bindCtx, noop } = func;
/**
* Select.AutoComplete
*/
class AutoComplete extends Base {
static propTypes = {
...Base.propTypes,
/**
* 当前值,用于受控模式
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* 初始化的默认值
*/
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Select发生改变时触发的回调
* @param {*} value 选中的值
* @param {String} actionType 触发的方式, 'itemClick', 'enter', 'change'
* @param {*} item 选中的值的对象数据
*/
onChange: PropTypes.func,
/**
* 传入的数据源,可以动态渲染子项
*/
dataSource: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.shape({
value: PropTypes.string,
label: PropTypes.any,
disabled: PropTypes.bool,
children: PropTypes.array,
}),
PropTypes.string,
])
),
/**
* 填充到选择框里的值的 key,默认是 value
*/
fillProps: PropTypes.string,
/**
* 渲染 MenuItem 内容的方法
* @param {Object} item 渲染节点的 item
* @return {ReactNode} item node
*/
itemRender: PropTypes.func,
// input keydown
onKeyDown: PropTypes.func,
// 是否将当前高亮的选项作为 placeholder
highlightHolder: PropTypes.bool,
style: PropTypes.object,
};
static defaultProps = {
...Base.defaultProps,
onKeyDown: noop,
fillProps: 'value',
};
constructor(props) {
super(props);
this.isAutoComplete = true;
this.isInputing = false;
bindCtx(this, [
'handleTriggerKeyDown',
'handleMenuSelect',
'handleItemClick',
]);
}
componentWillMount() {
this.dataStore.setOptions({ key: this.state.value });
super.componentWillMount();
}
componentWillReceiveProps(nextProps) {
if ('value' in nextProps) {
this.dataStore.setOptions({ key: nextProps.value });
this.setState({
value: nextProps.value,
});
}
if ('visible' in nextProps) {
this.setState({
visible: nextProps.visible,
});
}
this.dataStore.setOptions({
filter: nextProps.filter,
filterLocal: nextProps.filterLocal,
});
if (
nextProps.children !== this.props.children ||
nextProps.dataSource !== this.props.dataSource
) {
const dataSource = this.setDataSource(nextProps);
this.setState({
dataSource,
});
}
// remote dataSource and focused
// 因为autoComplete没有下拉数据不展示搜索并且有数据了需要自动展示下拉
if (!nextProps.filterLocal && this.isInputing) {
this.shouldControlPopup(nextProps, 'update');
}
if (!nextProps.filterLocal && !nextProps.popupContent) {
this.setFirstHightLightKeyForMenu();
}
}
componentWillUpdate() {
if (this.hasClear()) {
const inputNode = ReactDOM.findDOMNode(this.inputRef);
if (inputNode) {
this.clearNode = inputNode.querySelector(
`.${this.props.prefix}input-control`
);
}
}
}
shouldControlPopup(props = this.props, type) {
const hasPopup =
props.popupContent || this.dataStore.getMenuDS().length;
if (hasPopup) {
this.setVisible(true, type);
} else {
this.setVisible(false, type);
}
}
handleMenuSelect(keys) {
const key = keys[0];
const mapDS = this.dataStore.getMapDS();
if (key in mapDS) {
const item = mapDS[key];
this.handleSelectEvent(key, item, 'itemClick');
}
}
handleItemClick() {
this.setVisible(false, 'itemClick');
}
handleSelectEvent(key, item, triggerType) {
const value = (item && item[this.props.fillProps]) || key;
if (triggerType === 'itemClick' || triggerType === 'enter') {
// 点击 item 的时候不会触发关闭,需要手动关闭,其它类型比如 keyDown 等都会有其它事件句柄处理
this.setVisible(false, triggerType);
}
this.handleChange(value, triggerType, item);
}
handleChange = (value, proxy, item) => {
const { disabled, readOnly, filterLocal } = this.props;
if (disabled || readOnly) {
return false;
}
const actionType = typeof proxy === 'string' ? proxy : 'change';
this.isInputing = actionType === 'change';
if (filterLocal) {
this.setState({
dataSource: this.dataStore.updateByKey(value),
});
this.shouldControlPopup(this.props, actionType);
this.setFirstHightLightKeyForMenu();
}
// 非受控模式清空内部数据
if (!('value' in this.props)) {
this.setState({
value: value,
});
}
this.props.onChange(value, actionType, item);
if (actionType === 'itemClick' || actionType === 'enter') {
// 点击 item 的时候不会触发关闭,需要手动关闭,其它类型比如 keyDown 等都会有其它事件句柄处理
this.setVisible(false, actionType);
}
};
handleVisibleChange(visible, type) {
if (
!('visible' in this.props) &&
visible &&
!this.props.popupContent &&
!this.dataStore.getMenuDS().length
) {
return;
}
this.setVisible(visible, type);
}
beforeClose() {
this.isInputing = false;
}
/**
* Handle trigger keydown event
* @param {Event} e
*/
handleTriggerKeyDown(e) {
const { popupContent, onToggleHighlightItem, onKeyDown } = this.props;
if (popupContent) {
e.stopPropagation(); //stopPropagation can make use onChange triggerd while typing space('') in Input
return onKeyDown(e);
}
switch (e.keyCode) {
case KEYCODE.UP:
e.preventDefault();
onToggleHighlightItem(this.toggleHighlightItem(-1, e), 'up');
break;
case KEYCODE.DOWN:
e.preventDefault();
onToggleHighlightItem(this.toggleHighlightItem(1, e), 'down');
break;
case KEYCODE.ENTER:
e.preventDefault();
this.chooseHighlightItem(e);
break;
case KEYCODE.SPACE:
// stopPropagation can make use onChange triggerd while typing space('') in Input
e.stopPropagation();
break;
case KEYCODE.ESC:
e.preventDefault();
this.state.visible && this.setVisible(false, 'esc');
break;
default:
break;
}
onKeyDown(e);
}
// 回车 选择高亮的 item
chooseHighlightItem() {
if (!this.state.visible) {
return false;
}
const { highlightKey } = this.state;
const highlightItem = this.dataStore.getEnableDS().find(item => {
return highlightKey === `${item.value}`;
});
if (highlightItem) {
this.handleSelectEvent(highlightKey, highlightItem, 'enter');
}
}
hasClear() {
const { hasClear, readOnly, disabled } = this.props;
const { value } = this.state;
return value && hasClear && !readOnly && !disabled;
}
/**
* 选择器
* @override
* @param {object} props
*/
renderSelect(props = this.props) {
const {
placeholder,
size,
prefix,
className,
style,
label,
readOnly,
disabled,
highlightHolder,
locale,
hasClear,
state,
rtl,
} = props;
const others = obj.pickOthers(AutoComplete.propTypes, props);
const othersData = obj.pickAttrsWith(others, 'data-');
const value = this.state.value;
const visible = this.state.visible;
// // 下拉箭头
// const arrowNode = this.renderArrowNode(props, () => {
// this.focusInput();
// this.setVisible(!this.state.visible);
// });
// trigger className
const triggerClazz = classNames(
[
`${prefix}select`,
`${prefix}select-auto-complete`,
`${prefix}size-${size}`,
className,
],
{
[`${prefix}active`]: visible,
[`${prefix}disabled`]: disabled,
}
);
// highlightKey into placeholder
// compatible with selectPlaceHolder. TODO: removed in 2.0 version
let _placeholder =
placeholder ||
locale.autoCompletePlaceholder ||
locale.autoCompletePlaceHolder;
if (highlightHolder && visible) {
_placeholder = this.state.highlightKey || _placeholder;
}
// Input props
const _inputProps = {
...obj.pickOthers(othersData, others),
state: state,
ref: this.saveInputRef,
hasClear: hasClear,
value,
size,
disabled,
readOnly,
placeholder: _placeholder,
label,
// extra: arrowNode,
onChange: this.handleChange,
onKeyDown: this.handleTriggerKeyDown,
};
return (
<span
{...othersData}
className={triggerClazz}
style={style}
dir={rtl ? 'rtl' : undefined}
ref={this.saveSelectRef}
onClick={this.focusInput}
>
<Input
role="combobox"
aria-autocomplete="list"
aria-disabled={disabled}
aria-expanded={this.state.visible}
{..._inputProps}
/>
<span className={`${prefix}sr-only`} aria-live="polite">
{this.state.srReader}
</span>
</span>
);
}
render() {
if (this.hasClear()) {
// clear 按钮点击后,会在 dom 结构中被删除掉,需要将其额外设置为安全节点,防止触发弹层的显示或隐藏
const safeNode = this.props.popupProps.safeNode || [];
const safeNodes = Array.isArray(safeNode) ? safeNode : [safeNode];
safeNodes.push(() => this.clearNode);
this.props.popupProps.safeNode = safeNodes;
}
return super.render(
Object.assign({}, this.props, { canCloseByTrigger: false })
);
}
}
export default AutoComplete;