bee-select
Version:
select ui component for react
1,546 lines (1,445 loc) • 45.6 kB
JavaScript
/**
* This source code is quoted from rc-select.
* homepage: https://github.com/react-component/select
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { polyfill } from 'react-lifecycles-compat';
import { KeyCode, toArray as childrenToArray } from 'tinper-bee-core';
import classnames from 'classnames';
import Animate from 'bee-animate';
import classes from 'component-classes';
import { ItemGroup as MenuItemGroup } from 'rc-menu';
import MenuItem from 'rc-menu/lib/MenuItem'
import warning from 'warning';
import Option from './Option';
import omit from 'omit.js';
import {
getPropValue,
getValuePropValue,
isCombobox,
isMultipleOrTags,
isMultipleOrTagsOrCombobox,
isSingleMode,
toArray,
getMapKey,
findIndexInValueBySingleValue,
getLabelFromPropsValue,
UNSELECTABLE_ATTRIBUTE,
UNSELECTABLE_STYLE,
preventDefaultEvent,
findFirstMenuItem,
includesSeparators,
splitBySeparators,
defaultFilterFn,
validateOptionValue,
saveRef,
toTitle,
} from './util';
import SelectTrigger from './SelectTrigger';
import { SelectPropTypes } from './PropTypes';
function noop() {}
function chaining(...fns) {
return function (...args) { // eslint-disable-line
// eslint-disable-line
for (let i = 0; i < fns.length; i++) {
if (fns[i] && typeof fns[i] === 'function') {
fns[i].apply(this, args);
}
}
};
}
class Select extends React.Component {
static propTypes = SelectPropTypes;
static defaultProps = {
prefixCls: 'u-select',
defaultOpen: false,
labelInValue: false,
defaultActiveFirstOption: true,
showSearch: true,
allowClear: false,
placeholder: '',
onChange: noop,
onFocus: noop,
onBlur: noop,
onSelect: noop,
onSearch: noop,
onDeselect: noop,
onInputKeyDown: noop,
showArrow: true,
dropdownMatchSelectWidth: true,
dropdownStyle: {},
dropdownMenuStyle: {},
optionFilterProp: 'value',
optionLabelProp: 'value',
notFoundContent: 'Not Found',
backfill: false,
showAction: ['click'],
tokenSeparators: [],
autoClearSearchValue: true,
onKeyDown:noop
};
constructor(props) {
super(props);
const optionsInfo = Select.getOptionsInfoFromProps(props);
this.state = {
value: Select.getValueFromProps(props, true), // true: use default value
inputValue: props.combobox ? Select.getInputValueForCombobox(
props,
optionsInfo,
true, // use default value
) : '',
open: props.defaultOpen,
optionsInfo,
// a flag for aviod redundant getOptionsInfoFromProps call
skipBuildOptionsInfo: true,
};
this.saveInputRef = saveRef(this, 'inputRef');
this.saveInputMirrorRef = saveRef(this, 'inputMirrorRef');
this.saveTopCtrlRef = saveRef(this, 'topCtrlRef');
this.saveSelectTriggerRef = saveRef(this, 'selectTriggerRef');
this.saveRootRef = saveRef(this, 'rootRef');
this.saveSelectionRef = saveRef(this, 'selectionRef');
}
componentDidMount() {
if (this.props.autoFocus) {
this.focus();
}
}
componentDidUpdate() {
if (isMultipleOrTags(this.props)) {
const inputNode = this.getInputDOMNode();
const mirrorNode = this.getInputMirrorDOMNode();
if (inputNode.value) {
inputNode.style.width = '';
inputNode.style.width = `${mirrorNode.clientWidth}px`;
} else {
inputNode.style.width = '';
}
}
this.forcePopupAlign();
}
componentWillUnmount() {
this.clearFocusTime();
this.clearBlurTime();
if (this.dropdownContainer) {
ReactDOM.unmountComponentAtNode(this.dropdownContainer);
document.body.removeChild(this.dropdownContainer);
this.dropdownContainer = null;
}
}
onInputChange = event => {
const { tokenSeparators } = this.props;
const val = event.target.value;
if (
isMultipleOrTags(this.props) &&
tokenSeparators.length &&
includesSeparators(val, tokenSeparators)
) {
const nextValue = this.getValueByInput(val);
if (nextValue !== undefined) {
this.fireChange(nextValue);
}
this.setOpenState(false, true);
this.setInputValue('', false);
return;
}
this.setInputValue(val);
this.setState({
open: true,
});
if (isCombobox(this.props)) {
this.fireChange([val]);
}
};
onDropdownVisibleChange = open => {
if (open && !this._focused) {
this.clearBlurTime();
this.timeoutFocus();
this._focused = true;
this.updateFocusClassName();
}
this.setOpenState(open);
};
// combobox ignore
onKeyDown = event => {
const { open } = this.state;
const { disabled, onKeyDown,enterKeyDown } = this.props;
if (disabled) {
return;
}
const keyCode = event.keyCode;
if (open && !this.getInputDOMNode()) {
this.onInputKeyDown(event);
} else if (keyCode === KeyCode.DOWN) {
if (!open) {
this.setOpenState(true);
event.target._dataTransfer = {
_cancelBubble: true,
};
} else {
this.appendDataTransferToEvent(event)
}
event.preventDefault();
} else if (keyCode === KeyCode.UP) {
if (open) {
this.appendDataTransferToEvent(event)
}
event.preventDefault();
} else if(keyCode === KeyCode.ENTER){
if ((!open)&&enterKeyDown) this.setOpenState(true);
event.preventDefault();
}
onKeyDown(event);//sp
};
appendDataTransferToEvent = (event) => {
const { eventKey, children } = this.props;
const _eventKey = eventKey || '0-menu-';
const keyCode = event.keyCode;
const activeKeyKey = this.refs.menuItemRef.store.getState().activeKey[_eventKey];
const activeIndex = children.findIndex((data)=> data.key == activeKeyKey);
const activeIndexOld = children.findIndex((data)=> data.key == this.old_activeKeyKey);
// console.log('activeIndex', activeIndex, activeIndexOld);
if((keyCode === KeyCode.DOWN && activeIndex > activeIndexOld) || (keyCode === KeyCode.UP && (activeIndex < activeIndexOld || activeIndexOld == -1))) {
event.target._dataTransfer = {
_cancelBubble: true
};
}
this.old_activeKeyKey = activeKeyKey;
return activeIndex
}
onInputKeyDown = event => {
const props = this.props;
if (props.disabled) {
return;
}
const state = this.state;
const keyCode = event.keyCode;
if (
isMultipleOrTags(props) &&
!event.target.value &&
keyCode === KeyCode.BACKSPACE
) {
event.preventDefault();
const { value } = state;
if (value.length) {
this.removeSelected(value[value.length - 1]);
}
return;
}
if (keyCode === KeyCode.DOWN) {
if (!state.open) {
this.openIfHasChildren();
event.preventDefault();
event.stopPropagation();
return;
}
} else if (keyCode === KeyCode.ENTER && state.open) {
// Aviod trigger form submit when select item
// https://github.com/ant-design/ant-design/issues/10861
event.preventDefault();
} else if (keyCode === KeyCode.ESC) {
if (state.open) {
if (this.props.needFocusAfterSetOpenState) {
this.setOpenState(false, true);
} else {
this.setOpenState(false);
}
event.preventDefault();
event.stopPropagation();
}
if(props.showSearch)props.onKeyDown(event);//sp
return;
}
if (this.getRealOpenState(state)) {
const menu = this.selectTriggerRef.getInnerMenu();
if (menu && menu.onKeyDown(event, this.handleBackfill)) {
event.preventDefault();
event.stopPropagation();
}
}
};
// 选择下拉列表内容时调用
onMenuSelect = ({ item }) => {
if (!item) {
return;
}
let value = this.state.value;
const props = this.props;
const selectedValue = getValuePropValue(item);
const lastValue = value[value.length - 1];
this.fireSelect(selectedValue);
if (isMultipleOrTags(props)) {
if (findIndexInValueBySingleValue(value, selectedValue) !== -1) {
return;
}
value = value.concat([selectedValue]);
} else {
if (
lastValue !== undefined &&
lastValue === selectedValue &&
selectedValue !== this.state.backfillValue
) {
this.setOpenState(false, true);
return;
}
value = [selectedValue];
this.setOpenState(false, true);
}
this.fireChange(value);
let inputValue;
if (isCombobox(props)) {
inputValue = getPropValue(item, props.optionLabelProp);
} else {
inputValue = '';
}
if (props.autoClearSearchValue) {
this.setInputValue(inputValue, false);
}
};
onMenuDeselect = ({ item, domEvent }) => {
if (domEvent.type === 'keydown' && domEvent.keyCode === KeyCode.ENTER) {
this.removeSelected(getValuePropValue(item));
return;
}
if (domEvent.type === 'click') {
this.removeSelected(getValuePropValue(item));
}
const { props } = this;
if (props.autoClearSearchValue) {
this.setInputValue('', false);
}
};
onArrowClick = e => {
e.stopPropagation();
e.preventDefault();
this.props.onFocus(this.state.value);
if (!this.props.disabled) {
this.setOpenState(!this.state.open, !this.state.open);
}
};
onPlaceholderClick = () => {
if (this.getInputDOMNode()) {
this.getInputDOMNode().focus();
}
};
onOuterFocus = (e) => {
if (this.props.disabled) {
e.preventDefault();
return;
}
this.clearBlurTime();
if (
!isMultipleOrTagsOrCombobox(this.props) &&
e.target === this.getInputDOMNode()
) {
return;
}
if (this._focused) {
return;
}
this._focused = true;
this.updateFocusClassName();
if (!this._mouseDown) {
this.timeoutFocus();
}
};
onPopupFocus = () => {
// fix ie scrollbar, focus element again
this.maybeFocus(true, true);
};
onOuterBlur = (e) => {
if (this.props.disabled) {
e.preventDefault();
return;
}
this.blurTimer = setTimeout(() => {
this._focused = false;
this.updateFocusClassName();
const props = this.props;
let { value } = this.state;
const { inputValue } = this.state;
if (
isSingleMode(props) &&
props.showSearch &&
inputValue &&
props.defaultActiveFirstOption
) {
const options = this._options || [];
if (options.length) {
// const firstOption = findFirstMenuItem(options); // 自定义输入时失去焦点触发两次onChange解决
// if (firstOption) {
// value = [getValuePropValue(firstOption)];
// this.fireChange(value);
// }
if(props.showSearch&&props.supportWrite){//查询时是否支持自定义输入
value = [inputValue];
this.fireChange(value,true);
}
}
} else if (isMultipleOrTags(props) && inputValue) {
if (this._mouseDown) {
// need update dropmenu when not blur
this.setInputValue('');
} else {
// why not use setState?
this.state.inputValue = this.getInputDOMNode().value = '';
}
value = this.getValueByInput(inputValue);
if (value !== undefined) {
this.fireChange(value);
}
}
// if click the rest space of Select in multiple mode
if (isMultipleOrTags(props) && this._mouseDown) {
this.maybeFocus(true, true);
this._mouseDown = false;
return;
}
this.setOpenState(false);
props.onBlur(this.getVLForOnChange(value));
}, 10);
};
onClearSelection = event => {
const props = this.props;
const state = this.state;
if (props.disabled) {
return;
}
const { inputValue, value } = state;
event.stopPropagation();
if (inputValue || value.length) {
if (value.length) {
this.fireChange([]);
}
this.setOpenState(false, true);
if (inputValue) {
this.setInputValue('');
}
}
};
onChoiceAnimationLeave = () => {
this.forcePopupAlign();
};
static getDerivedStateFromProps = (nextProps, prevState) => {
const optionsInfo = prevState.skipBuildOptionsInfo
? prevState.optionsInfo
: Select.getOptionsInfoFromProps(nextProps, prevState);
const newState = {
optionsInfo,
skipBuildOptionsInfo: false,
};
if ('open' in nextProps) {
newState.open = nextProps.open;
}
if ('value' in nextProps) {
const value = Select.getValueFromProps(nextProps);
newState.value = value;
if (nextProps.combobox) {
newState.inputValue = Select.getInputValueForCombobox(
nextProps,
optionsInfo,
);
}
}
return newState;
};
static getOptionsFromChildren = (children, options = []) => {
React.Children.forEach(children, child => {
if (!child) {
return;
}
if (child&&child.type&&child.type.isSelectOptGroup) {
Select.getOptionsFromChildren(child.props.children, options);
} else {
options.push(child);
}
});
return options;
};
static getInputValueForCombobox = (props, optionsInfo, useDefaultValue) => {
let value = [];
if ('value' in props && !useDefaultValue) {
value = toArray(props.value);
}
if ('defaultValue' in props && useDefaultValue) {
value = toArray(props.defaultValue);
}
if (value.length) {
value = value[0];
} else {
return '';
}
let label = value;
if (props.labelInValue) {
label = value.label;
} else if (optionsInfo[getMapKey(value)]) {
label = optionsInfo[getMapKey(value)].label;
}
if (label === undefined) {
label = '';
}
return label;
}
static getLabelFromOption = (props, option) => {
return getPropValue(option, props.optionLabelProp);
};
static getOptionsInfoFromProps = (props, preState) => {
const options = Select.getOptionsFromChildren(props.children);
const optionsInfo = {};
options.forEach((option) => {
const singleValue = getValuePropValue(option);
optionsInfo[getMapKey(singleValue)] = {
option,
value: singleValue,
label: Select.getLabelFromOption(props, option),
title: option.props.title,
};
});
if (preState) {
// keep option info in pre state value.
const oldOptionsInfo = preState.optionsInfo;
const value = preState.value;
value.forEach(v => {
const key = getMapKey(v);
if (!optionsInfo[key] && oldOptionsInfo[key] !== undefined) {
optionsInfo[key] = oldOptionsInfo[key];
}
});
}
return optionsInfo;
}
static getValueFromProps = (props, useDefaultValue) => {
let value = [];
if ('value' in props && !useDefaultValue) {
value = toArray(props.value);
}
if ('defaultValue' in props && useDefaultValue) {
value = toArray(props.defaultValue);
}
if (props.labelInValue) {
value = value.map((v) => {
return v.key;
});
}
return value;
}
getOptionInfoBySingleValue = (value, optionsInfo) => {
let info;
optionsInfo = optionsInfo || this.state.optionsInfo;
if (optionsInfo[getMapKey(value)]) {
info = optionsInfo[getMapKey(value)];
}
if (info) {
return info;
}
let defaultLabel = value;
if (this.props.labelInValue) {
const label = getLabelFromPropsValue(this.props.value, value);
if (label !== undefined) {
defaultLabel = label;
}
}
const defaultInfo = {
option: <Option value={value} key={value}>{value}</Option>,
value,
label: defaultLabel,
};
return defaultInfo;
}
getOptionBySingleValue = value => {
const { option } = this.getOptionInfoBySingleValue(value);
return option;
}
getOptionsBySingleValue = values => {
return values.map(value => {
return this.getOptionBySingleValue(value);
});
}
getValueByLabel = (label) => {
if (label === undefined) {
return null;
}
let value = null;
Object.keys(this.state.optionsInfo).forEach(key => {
const info = this.state.optionsInfo[key];
if (toArray(info.label).join('') === label) {
value = info.value;
}
});
return value;
};
getVLBySingleValue = value => {
if (this.props.labelInValue) {
return {
key: value,
label: this.getLabelBySingleValue(value),
};
}
return value;
}
getVLForOnChange = vls_ => {
let vls = vls_;
if (vls !== undefined) {
if (!this.props.labelInValue) {
vls = vls.map(v => v);
} else {
vls = vls.map(vl => ({
key: vl,
label: this.getLabelBySingleValue(vl),
}));
}
return isMultipleOrTags(this.props) ? vls : vls[0];
}
return vls;
};
getLabelBySingleValue = (value, optionsInfo) => {
const { label } = this.getOptionInfoBySingleValue(value, optionsInfo);
return label;
};
getDropdownContainer = () => {
if (!this.dropdownContainer) {
this.dropdownContainer = document.createElement('div');
document.body.appendChild(this.dropdownContainer);
}
return this.dropdownContainer;
};
getPlaceholderElement = () => {
const { props, state } = this;
let hidden = false;
if (state.inputValue) {
hidden = true;
}
if (state.value[0]) {
hidden = true;
}
if (isCombobox(props) && state.value.length === 1 && !state.value[0]) {
hidden = false;
}
const placeholder = props.placeholder;
if (placeholder) {
return (
<div
onMouseDown={preventDefaultEvent}
style={{
display: hidden ? 'none' : 'block',
...UNSELECTABLE_STYLE,
}}
{...UNSELECTABLE_ATTRIBUTE}
onClick={this.onPlaceholderClick}
className={`${props.prefixCls}-selection-placeholder`}
>
{placeholder}
</div>
);
}
return null;
};
getInputElement = () => {
const props = this.props;
const inputElement = props.getInputElement
? props.getInputElement()
: <input id={props.id} autoComplete="off" />;
const inputCls = classnames(inputElement.props.className, {
[`${props.prefixCls}-search-field`]: true,
});
// https://github.com/ant-design/ant-design/issues/4992#issuecomment-281542159
// Add space to the end of the inputValue as the width measurement tolerance
return (
<div className={`${props.prefixCls}-search-field-wrap`}>
{React.cloneElement(inputElement, {
ref: this.saveInputRef,
onChange: this.onInputChange,
onKeyDown: chaining(
this.onInputKeyDown,
inputElement.props.onKeyDown,
this.props.onInputKeyDown
),
value: this.state.inputValue,
disabled: props.disabled,
className: inputCls,
})}
<span
ref={this.saveInputMirrorRef}
className={`${props.prefixCls}-search-field-mirror`}
>
{this.state.inputValue}
</span>
</div>
);
};
getInputDOMNode = () => {
return this.topCtrlRef
? this.topCtrlRef.querySelector('input,textarea,div[contentEditable]')
: this.inputRef;
};
getInputMirrorDOMNode = () => {
return this.inputMirrorRef;
};
getPopupDOMNode = () => {
return this.selectTriggerRef.getPopupDOMNode();
};
getPopupMenuComponent = () => {
return this.selectTriggerRef.getInnerMenu();
};
setOpenState = (open, needFocus) => {
const { props, state } = this;
if (state.open === open) {
this.maybeFocus(open, needFocus);
return;
}
if (this.props.onDropdownVisibleChange) {
this.props.onDropdownVisibleChange(open);
}
const nextState = {
open,
backfillValue: undefined,
};
// clear search input value when open is false in singleMode.
if (!open && isSingleMode(props) && props.showSearch) {
this.setInputValue('', false);
}
if (!open) {
this.maybeFocus(open, needFocus);
}
this.setState(nextState, () => {
if (open) {
this.maybeFocus(open, needFocus);
}
});
};
setInputValue = (inputValue, fireSearch = true) => {
if (inputValue !== this.state.inputValue) {
this.setState({
inputValue,
}, this.forcePopupAlign);
if (fireSearch) {
this.props.onSearch(inputValue);
}
}
};
getValueByInput = string => {
const { multiple, tokenSeparators } = this.props;
let nextValue = this.state.value;
let hasNewValue = false;
splitBySeparators(string, tokenSeparators).forEach(label => {
const selectedValue = [label];
if (multiple) {
const value = this.getValueByLabel(label);
if (value && findIndexInValueBySingleValue(nextValue, value) === -1) {
nextValue = nextValue.concat(value);
hasNewValue = true;
this.fireSelect(value);
}
} else {
// tag
if (findIndexInValueBySingleValue(nextValue, label) === -1) {
nextValue = nextValue.concat(selectedValue);
hasNewValue = true;
this.fireSelect(label);
}
}
});
return hasNewValue ? nextValue : undefined;
};
getRealOpenState = (state) => {
const { open: _open } = this.props;
if (typeof _open === 'boolean') {
return _open;
}
let open = (state || this.state).open;
const options = this._options || [];
if (isMultipleOrTagsOrCombobox(this.props) || !this.props.showSearch) {
if (open && !options.length) {
open = false;
}
}
return open;
}
focus() {
if (isSingleMode(this.props)) {
this.selectionRef.focus();
} else {
this.getInputDOMNode().focus();
}
}
blur() {
if (isSingleMode(this.props)) {
this.selectionRef.blur();
} else {
this.getInputDOMNode().blur();
}
}
markMouseDown = () => {
this._mouseDown = true;
};
markMouseLeave = () => {
this._mouseDown = false;
}
handleBackfill = (item) => {
if (!this.props.backfill || !(isSingleMode(this.props) || isCombobox(this.props))) {
return;
}
const key = getValuePropValue(item);
if (isCombobox(this.props)) {
this.setInputValue(key, false);
}
this.setState({
value: [key],
backfillValue: key,
});
};
filterOption = (input, child, defaultFilter = defaultFilterFn) => {
const { value } = this.state;
const lastValue = value[value.length - 1];
if (!input || (lastValue && lastValue === this.state.backfillValue)) {
return true;
}
let filterFn = this.props.filterOption;
if ('filterOption' in this.props) {
if (this.props.filterOption === true) {
filterFn = defaultFilter;
}
} else {
filterFn = defaultFilter;
}
if (!filterFn) {
return true;
} else if (typeof filterFn === 'function') {
return filterFn.call(this, input, child);
} else if (child.props.disabled) {
return false;
}
return true;
};
timeoutFocus = () => {
if (this.focusTimer) {
this.clearFocusTime();
}
this.focusTimer = setTimeout(() => {
this.props.onFocus();
}, 10);
};
clearFocusTime = () => {
if (this.focusTimer) {
clearTimeout(this.focusTimer);
this.focusTimer = null;
}
};
clearBlurTime = () => {
if (this.blurTimer) {
clearTimeout(this.blurTimer);
this.blurTimer = null;
}
};
updateFocusClassName = () => {
const { rootRef, props } = this;
// avoid setState and its side effect
if (this._focused) {
classes(rootRef).add(`${props.prefixCls}-focused`);
} else {
classes(rootRef).remove(`${props.prefixCls}-focused`);
}
};
maybeFocus = (open, needFocus) => {
if (needFocus || open) {
const input = this.getInputDOMNode();
const { activeElement } = document;
if (input && (open || isMultipleOrTagsOrCombobox(this.props))) {
if (activeElement !== input) {
input.focus();
this._focused = true;
}
} else if (activeElement !== this.selectionRef) {
this.selectionRef.focus();
this._focused = true;
}
}
};
removeSelected = (selectedKey, e) => {
const props = this.props;
if (props.disabled || this.isChildDisabled(selectedKey)) {
return;
}
// Do not trigger Trigger popup
if (e && e.stopPropagation) {
e.stopPropagation();
}
const value = this.state.value.filter(singleValue => {
return singleValue !== selectedKey;
});
const canMultiple = isMultipleOrTags(props);
if (canMultiple) {
let event = selectedKey;
if (props.labelInValue) {
event = {
key: selectedKey,
label: this.getLabelBySingleValue(selectedKey),
};
}
props.onDeselect(event, this.getOptionBySingleValue(selectedKey));
}
this.fireChange(value);
};
openIfHasChildren = () => {
const props = this.props;
if (React.Children.count(props.children) || isSingleMode(props)) {
this.setOpenState(true);
}
};
fireSelect = value => {
this.props.onSelect(this.getVLBySingleValue(value), this.getOptionBySingleValue(value));
};
/**
* noCheck 判断输入的值是否不需要匹配option
*/
fireChange = (value,noCheck) => {
const props = this.props;
if (!('value' in props)) {
this.setState({
value,
}, this.forcePopupAlign);
}
if(noCheck){
props.onChange(value,null);
}else{
const vls = this.getVLForOnChange(value);
const options = this.getOptionsBySingleValue(value);
props.onChange(vls, isMultipleOrTags(this.props) ? options : options[0]);
}
};
isChildDisabled = key => {
return childrenToArray(this.props.children).some(child => {
const childValue = getValuePropValue(child);
return childValue === key && child.props && child.props.disabled;
});
};
// 超出长度时hover "+n" 显示的option的名称
getOptionName = (optionsInfo, maxTagCount, value) => {
let titleArr = [];
let arr = value.slice(maxTagCount)
Object.keys(optionsInfo).forEach(function(key, index){
let optionItem = optionsInfo[key];
if (arr.includes(optionItem.value)){
titleArr.push(optionItem.label);
}
});
return titleArr;
}
forcePopupAlign = () => {
if (!this.state.open) {
return;
}
if(this.selectTriggerRef&&this.selectTriggerRef.triggerRef&&this.selectTriggerRef.triggerRef.forcePopupAlign){
if(typeof this.selectTriggerRef.triggerRef.forcePopupAlign =='function'){
this.selectTriggerRef.triggerRef.forcePopupAlign()
}
}
};
renderFilterOptions = () => {
const { inputValue } = this.state;
const { children, tags, filterOption, notFoundContent } = this.props;
const menuItems = [];
const childrenKeys = [];
let options = this.renderFilterOptionsFromChildren(
children,
childrenKeys,
menuItems,
);
if (tags) {
// tags value must be string
let value = this.state.value;
value = value.filter(singleValue => {
return (
childrenKeys.indexOf(singleValue) === -1 &&
(!inputValue ||
String(singleValue).indexOf(String(inputValue)) > -1)
);
});
value.forEach(singleValue => {
const key = singleValue;
const menuItem = (
<MenuItem
style={UNSELECTABLE_STYLE}
role="option"
attribute={UNSELECTABLE_ATTRIBUTE}
value={key}
key={key}
>
{key}
</MenuItem>
);
options.push(menuItem);
menuItems.push(menuItem);
});
if (inputValue) {
const notFindInputItem = menuItems.every(option => {
// this.filterOption return true has two meaning,
// 1, some one exists after filtering
// 2, filterOption is set to false
// condition 2 does not mean the option has same value with inputValue
const filterFn = () => getValuePropValue(option) === inputValue;
if (filterOption !== false) {
return !this.filterOption.call(
this,
inputValue,
option,
filterFn
);
}
return !filterFn();
});
if (notFindInputItem) {
options.unshift(
<MenuItem
style={UNSELECTABLE_STYLE}
role="option"
attribute={UNSELECTABLE_ATTRIBUTE}
value={inputValue}
key={inputValue}
>
{inputValue}
</MenuItem>
);
}
}
}
if (!options.length && notFoundContent) {
options = [
<MenuItem
style={UNSELECTABLE_STYLE}
attribute={UNSELECTABLE_ATTRIBUTE}
disabled
role="option"
value="NOT_FOUND"
key="NOT_FOUND"
>
{notFoundContent}
</MenuItem>,
];
}
return options;
};
renderFilterOptionsFromChildren = (children, childrenKeys, menuItems) => {
const sel = [];
const props = this.props;
const { inputValue } = this.state;
const tags = props.tags;
React.Children.forEach(children, child => {
if (!child) {
return;
}
if (child.type.isSelectOptGroup) {
const innerItems = this.renderFilterOptionsFromChildren(
child.props.children,
childrenKeys,
menuItems,
);
if (innerItems.length) {
let label = child.props.label;
let key = child.key;
if (!key && typeof label === 'string') {
key = label;
} else if (!label && key) {
label = key;
}
sel.push(
<MenuItemGroup key={key} title={label}>
{innerItems}
</MenuItemGroup>
);
}
return;
}
warning(
child.type.isSelectOption,
'the children of `Select` should be `Select.Option` or `Select.OptGroup`, ' +
`instead of \`${child.type.name ||
child.type.displayName ||
child.type}\`.`
);
const childValue = getValuePropValue(child);
validateOptionValue(childValue, this.props);
if (this.filterOption(inputValue, child)) {
const menuItem = (
<MenuItem
ref="menuItemRef"
style={UNSELECTABLE_STYLE}
attribute={UNSELECTABLE_ATTRIBUTE}
value={childValue}
key={childValue}
role="option"
{...child.props}
/>
);
sel.push(menuItem);
menuItems.push(menuItem);
}
if (tags) {
childrenKeys.push(childValue);
}
});
return sel;
};
renderTopControlNode = () => {
const { value, open, inputValue, optionsInfo } = this.state;
const props = this.props;
const {
choiceTransitionName,
prefixCls,
maxTagTextLength,
maxTagCount,
maxTagPlaceholder,
showSearch,
removeIcon,
userSelectText // 是否可以选中输入框内的文字,如果为true 样式user-select设置为text
} = props;
const className = `${prefixCls}-selection-rendered`;
// search input is inside topControlNode in single, multiple & combobox. 2016/04/13
let innerNode = null;
if (isSingleMode(props)) {
let selectedValue = null;
if (value.length) {
let showSelectedValue = false;
let opacity = 1;
if (!showSearch) {
showSelectedValue = true;
} else if (open) {
showSelectedValue = !inputValue;
if (showSelectedValue) {
opacity = 0.4;
}
} else {
showSelectedValue = true;
}
const singleValue = value[0];
const { label, title } = this.getOptionInfoBySingleValue(singleValue);
selectedValue = (
<div
key="value"
className={`${prefixCls}-selection-selected-value`}
title={toTitle(title || label)}
style={{
display: showSelectedValue ? 'block' : 'none',
opacity,
}}
>
{label}
</div>
);
}
if (!showSearch) {
innerNode = [selectedValue];
} else {
innerNode = [
selectedValue,
<div
className={`${prefixCls}-search ${prefixCls}-search--inline`}
key="input"
style={{
display: open ? 'block' : 'none',
}}
>
{this.getInputElement()}
</div>,
];
}
} else {
let selectedValueNodes = [];
let limitedCountValue = value;
let maxTagPlaceholderEl;
if (maxTagCount !== undefined && value.length > maxTagCount) {
limitedCountValue = limitedCountValue.slice(0, maxTagCount);
let title = this.getOptionName(optionsInfo, maxTagCount, value); // 获取option的中文名
// const omittedValues = this.getVLForOnChange(title.slice(maxTagCount, value.length)); // 截取hover时需要显示的数组
let content = `+ ${value.length - maxTagCount} ...`;
if (maxTagPlaceholder) {
content = typeof maxTagPlaceholder === 'function' ?
maxTagPlaceholder(title) : maxTagPlaceholder;
}
//超过最大长度显示的内容
maxTagPlaceholderEl = (<li
style={UNSELECTABLE_STYLE}
{...UNSELECTABLE_ATTRIBUTE}
onMouseDown={preventDefaultEvent}
className={`${prefixCls}-selection-choice ${prefixCls}-selection-choice-disabled`}
key="maxTagPlaceholder"
title={toTitle(title)}
>
<div className={`${prefixCls}-selection-choice-content`}>{content}</div>
</li>);
}
if (isMultipleOrTags(props)) {
selectedValueNodes = limitedCountValue.map((singleValue,index) => {
let key = singleValue||index;
const info = this.getOptionInfoBySingleValue(singleValue);
let content = info.label;
const title = info.title || content;
if (
maxTagTextLength &&
typeof content === 'string' &&
content.length > maxTagTextLength
) {
content = `${content.slice(0, maxTagTextLength)}...`;
}
const disabled = this.isChildDisabled(singleValue);
const choiceClassName = disabled
? `${prefixCls}-selection-choice ${prefixCls}-selection-choice-disabled`
: `${prefixCls}-selection-choice`;
return (
<li
style={UNSELECTABLE_STYLE}
{...UNSELECTABLE_ATTRIBUTE}
onMouseDown={preventDefaultEvent}
className={choiceClassName}
key={key}
title={toTitle(title)}
>
<div className={`${prefixCls}-selection-choice-content`}>
{content}
</div>
{disabled ? null : (
<span
onClick={(event) => {
this.removeSelected(singleValue, event);
}}
className={`${prefixCls}-selection-choice-remove`}
>
{removeIcon || <i className={`${prefixCls}-selection-choice-remove-icon`}>×</i>}
</span>
)}
</li>
);
});
}
if (maxTagPlaceholderEl) {
selectedValueNodes.push(maxTagPlaceholderEl);
}
selectedValueNodes.push(
<li
className={`${prefixCls}-search ${prefixCls}-search--inline`}
key="-input"
>
{this.getInputElement()}
</li>
);
if (isMultipleOrTags(props) && choiceTransitionName) {
innerNode = (
<Animate
onLeave={this.onChoiceAnimationLeave}
component="ul"
transitionName={choiceTransitionName}
>
{selectedValueNodes}
</Animate>
);
} else {
innerNode = (
<ul>
{selectedValueNodes}
</ul>
);
}
}
return (
<div className={className} ref={this.saveTopCtrlRef} style={{userSelect: userSelectText ? "text" : null}}>
{this.getPlaceholderElement()}
{innerNode}
</div>
);
}
renderClear() {
const { prefixCls, allowClear, clearIcon } = this.props;
const { value, inputValue } = this.state;
const clear = (
<span
key="clear"
className={`${prefixCls}-selection-clear`}
onMouseDown={preventDefaultEvent}
style={UNSELECTABLE_STYLE}
{...UNSELECTABLE_ATTRIBUTE}
onClick={this.onClearSelection}
>{clearIcon || <i className={`${prefixCls}-selection-clear-icon`}></i>}</span>
);
if (!allowClear) {
return null;
}
if (isCombobox(this.props)) {
if (inputValue) {
return clear;
}
return null;
}
if (inputValue || value.length) {
return clear;
}
return null;
}
onOuterClick=(event)=>{//sp:在不能输入的情况下,点击事件触发focus
this._focused = true;
this.updateFocusClassName();
this.props.onFocus(this.state.value);
}
render() {
const props = this.props;
const multiple = isMultipleOrTags(props);
const state = this.state;
const { className, disabled, prefixCls, inputIcon } = props;
const ctrlNode = this.renderTopControlNode();
const { open } = this.state;
if (open) {
this._options = this.renderFilterOptions();
}
const realOpen = this.getRealOpenState();
const options = this._options || [];
const dataOrAriaAttributeProps = {};
const customProps = {...omit(props, [
'transitionName',
'choiceTransitionName',
'optionLabelProp',
'notFoundContent',
'clsPrefix',
'prefixCls',
'placeholder',
'dropdownStyle',
'dropdownMenuStyle',
'optionFilterProp',
'showAction',
'tokenSeparators',
'showSearch',
'allowClear',
'enterKeyDown',
'defaultOpen',
'labelInValue',
'defaultActiveFirstOption',
'onSearch',
'onDeselect',
'onInputKeyDown',
'showArrow',
'dropdownMatchSelectWidth',
'autoClearSearchValue',
'searchPlaceholder',
'scrollToEnd',
'filterOption',
'backfill',
'tags',
'combobox',
'supportWrite',
'onChange',
'onFocus',
'onBlur',
'onSelect',
'onSearch',
'onDeselect',
'onInputKeyDown',
'onKeyDown',
'userSelectText'
])};
for (const key in props) {
if (
Object.prototype.hasOwnProperty.call(props, key) &&
(key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role')
) {
dataOrAriaAttributeProps[key] = props[key];
}
}
let extraSelectionProps = { ...dataOrAriaAttributeProps };
if (!isMultipleOrTagsOrCombobox(props)) {
extraSelectionProps = {
...extraSelectionProps,
onKeyDown: this.onKeyDown,
tabIndex: props.disabled ? -1 : 0,
};
}
const rootCls = {
[className]: !!className,
[prefixCls]: 1,
[`${prefixCls}-open`]: open,
[`${prefixCls}-focused`]: open || !!this._focused,
[`${prefixCls}-combobox`]: isCombobox(props),
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-enabled`]: !disabled,
[`${prefixCls}-allow-clear`]: !!props.allowClear,
[`${prefixCls}-no-arrow`]: !props.showArrow,
};
return (
<SelectTrigger
open = {props.open}
onPopupFocus={this.onPopupFocus}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
dropdownAlign={props.dropdownAlign}
dropdownClassName={props.dropdownClassName}
dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
defaultActiveFirstOption={props.defaultActiveFirstOption}
dropdownMenuStyle={props.dropdownMenuStyle}
transitionName={props.transitionName}
animation={props.animation}
prefixCls={props.prefixCls}
dropdownStyle={props.dropdownStyle}
combobox={props.combobox}
showSearch={props.showSearch}
options={options}
multiple={multiple}
disabled={disabled}
visible={realOpen}
inputValue={state.inputValue}
value={state.value}
backfillValue={state.backfillValue}
firstActiveValue={props.firstActiveValue}
onDropdownVisibleChange={this.onDropdownVisibleChange}
getPopupContainer={props.getPopupContainer}
onMenuSelect={this.onMenuSelect}
onMenuDeselect={this.onMenuDeselect}
onPopupScroll={props.onPopupScroll}
showAction={props.showAction}
ref={this.saveSelectTriggerRef}
clsPrefix={`${props.clsPrefix}-dropdown`}
menuItemSelectedIcon={props.menuItemSelectedIcon}
popData={props.popData}
>
<div
{...customProps}
id={props.id}
style={props.style}
ref={this.saveRootRef}
onBlur={this.onOuterBlur}
onFocus={this.onOuterFocus}
onClick={this.onOuterClick}//sp
className={classnames(rootCls)}
onMouseDown={this.markMouseDown}
onMouseUp={this.markMouseLeave}
onMouseOut={this.markMouseLeave}
>
<div
ref={this.saveSelectionRef}
key="selection"
className={`${prefixCls}-selection
${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`}
role="combobox"
aria-autocomplete="list"
aria-haspopup="true"
aria-expanded={realOpen}
{...extraSelectionProps}
>
{ctrlNode}
{this.renderClear()}
{!props.showArrow ? null : (//sp
<span
key="arrow"
className={`${prefixCls}-arrow`}
style={UNSELECTABLE_STYLE}
{...UNSELECTABLE_ATTRIBUTE}
onClick={this.onArrowClick}
>
{inputIcon || <i className={`${prefixCls}-arrow-icon`} />}
</span>)}
</div>
</div>
</SelectTrigger>
);
}
}
Select.displayName = 'Select';
polyfill(Select);
export default Select;