You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

610 lines
18 KiB

import * as React from 'react';
import RcCascader from 'rc-cascader';
import arrayTreeFilter from 'array-tree-filter';
import classNames from 'classnames';
import omit from 'omit.js';
import KeyCode from 'rc-util/lib/KeyCode';
import { polyfill } from 'react-lifecycles-compat';
import Input from '../input';
import Icon from '../icon';
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import warning from '../_util/warning';
export interface CascaderOptionType {
value?: string;
label?: React.ReactNode;
disabled?: boolean;
isLeaf?: boolean;
loading?: boolean;
children?: Array<CascaderOptionType>;
[key: string]: any;
}
export interface FieldNamesType {
value?: string;
label?: string;
children?: string;
}
export interface FilledFieldNamesType {
value: string;
label: string;
children: string;
}
export type CascaderExpandTrigger = 'click' | 'hover';
export interface ShowSearchType {
filter?: (inputValue: string, path: CascaderOptionType[], names: FilledFieldNamesType) => boolean;
render?: (
inputValue: string,
path: CascaderOptionType[],
prefixCls: string | undefined,
names: FilledFieldNamesType,
) => React.ReactNode;
sort?: (
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) => number;
matchInputWidth?: boolean;
limit?: number | false;
}
export interface CascaderProps {
/** 可选项数据源 */
options: CascaderOptionType[];
/** 默认的选中项 */
defaultValue?: string[];
/** 指定选中项 */
value?: string[];
/** 选择完成后的回调 */
onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
/** 选择后展示的渲染函数 */
displayRender?: (label: string[], selectedOptions?: CascaderOptionType[]) => React.ReactNode;
/** 自定义样式 */
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
/** 自定义浮层类名 */
popupClassName?: string;
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
popupPlacement?: string;
/** 输入框占位文本 */
placeholder?: string;
/** 输入框大小,可选 `large` `default` `small` */
size?: string;
/** 禁用 */
disabled?: boolean;
/** 是否支持清除 */
allowClear?: boolean;
showSearch?: boolean | ShowSearchType;
notFoundContent?: React.ReactNode;
loadData?: (selectedOptions?: CascaderOptionType[]) => void;
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger?: CascaderExpandTrigger;
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
changeOnSelect?: boolean;
/** 浮层可见变化时回调 */
onPopupVisibleChange?: (popupVisible: boolean) => void;
prefixCls?: string;
inputPrefixCls?: string;
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
popupVisible?: boolean;
/** use this after antd@3.7.0 */
fieldNames?: FieldNamesType;
/** typo props name before antd@3.7.0 */
filedNames?: FieldNamesType;
suffixIcon?: React.ReactNode;
}
export interface CascaderState {
inputFocused: boolean;
inputValue: string;
value: string[];
popupVisible: boolean | undefined;
flattenOptions: CascaderOptionType[][] | undefined;
prevProps: CascaderProps;
}
interface CascaderLocale {
placeholder?: string;
}
// We limit the filtered item count by default
const defaultLimit = 50;
function highlightKeyword(str: string, keyword: string, prefixCls: string | undefined) {
return str.split(keyword).map((node: string, index: number) =>
index === 0
? node
: [
<span className={`${prefixCls}-menu-item-keyword`} key="seperator">
{keyword}
</span>,
node,
],
);
}
function defaultFilterOption(
inputValue: string,
path: CascaderOptionType[],
names: FilledFieldNamesType,
) {
return path.some(option => (option[names.label] as string).indexOf(inputValue) > -1);
}
function defaultRenderFilteredOption(
inputValue: string,
path: CascaderOptionType[],
prefixCls: string | undefined,
names: FilledFieldNamesType,
) {
return path.map((option, index) => {
const label = option[names.label];
const node =
(label as string).indexOf(inputValue) > -1
? highlightKeyword(label as string, inputValue, prefixCls)
: label;
return index === 0 ? node : [' / ', node];
});
}
function defaultSortFilteredOption(
a: CascaderOptionType[],
b: CascaderOptionType[],
inputValue: string,
names: FilledFieldNamesType,
) {
function callback(elem: CascaderOptionType) {
return (elem[names.label] as string).indexOf(inputValue) > -1;
}
return a.findIndex(callback) - b.findIndex(callback);
}
function getFieldNames(props: CascaderProps) {
const { fieldNames, filedNames } = props;
if ('filedNames' in props) {
return filedNames; // For old compatibility
}
return fieldNames;
}
function getFilledFieldNames(props: CascaderProps) {
const fieldNames = getFieldNames(props) || {};
const names: FilledFieldNamesType = {
children: fieldNames.children || 'children',
label: fieldNames.label || 'label',
value: fieldNames.value || 'value',
};
return names;
}
function flattenTree(
options: CascaderOptionType[],
props: CascaderProps,
ancestor: CascaderOptionType[] = [],
) {
const names: FilledFieldNamesType = getFilledFieldNames(props);
let flattenOptions: CascaderOptionType[][] = [];
const childrenName = names.children;
options.forEach(option => {
const path = ancestor.concat(option);
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
flattenOptions.push(path);
}
if (option[childrenName]) {
flattenOptions = flattenOptions.concat(flattenTree(option[childrenName], props, path));
}
});
return flattenOptions;
}
const defaultDisplayRender = (label: string[]) => label.join(' / ');
function warningValueNotExist(list: CascaderOptionType[], fieldNames: FieldNamesType = {}) {
(list || []).forEach(item => {
const valueFieldName = fieldNames.value || 'value';
warning(valueFieldName in item, 'Cascader', 'Not found `value` in `options`.');
warningValueNotExist(item[fieldNames.children || 'children'], fieldNames);
});
}
class Cascader extends React.Component<CascaderProps, CascaderState> {
static defaultProps = {
transitionName: 'slide-up',
popupPlacement: 'bottomLeft',
options: [],
disabled: false,
allowClear: true,
};
static getDerivedStateFromProps(nextProps: CascaderProps, { prevProps }: CascaderState) {
const newState: Partial<CascaderState> = {
prevProps: nextProps,
};
if ('value' in nextProps) {
newState.value = nextProps.value || [];
}
if ('popupVisible' in nextProps) {
newState.popupVisible = nextProps.popupVisible;
}
if (nextProps.showSearch && prevProps.options !== nextProps.options) {
newState.flattenOptions = flattenTree(nextProps.options, nextProps);
}
if (process.env.NODE_ENV !== 'production' && nextProps.options) {
warningValueNotExist(nextProps.options, getFieldNames(nextProps));
}
return newState;
}
cachedOptions: CascaderOptionType[] = [];
private input: Input;
constructor(props: CascaderProps) {
super(props);
this.state = {
value: props.value || props.defaultValue || [],
inputValue: '',
inputFocused: false,
popupVisible: props.popupVisible,
flattenOptions: props.showSearch ? flattenTree(props.options, props) : undefined,
prevProps: props,
};
}
setValue = (value: string[], selectedOptions: CascaderOptionType[] = []) => {
if (!('value' in this.props)) {
this.setState({ value });
}
const { onChange } = this.props;
if (onChange) {
onChange(value, selectedOptions);
}
};
getLabel() {
const { options, displayRender = defaultDisplayRender as Function } = this.props;
const names = getFilledFieldNames(this.props);
const { value } = this.state;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
const selectedOptions: CascaderOptionType[] = arrayTreeFilter(
options,
(o: CascaderOptionType, level: number) => o[names.value] === unwrappedValue[level],
{ childrenKeyName: names.children },
);
const label = selectedOptions.map(o => o[names.label]);
return displayRender(label, selectedOptions);
}
saveInput = (node: Input) => {
this.input = node;
};
handleChange = (value: any, selectedOptions: CascaderOptionType[]) => {
this.setState({ inputValue: '' });
if (selectedOptions[0].__IS_FILTERED_OPTION) {
const unwrappedValue = value[0];
const unwrappedSelectedOptions = selectedOptions[0].path;
this.setValue(unwrappedValue, unwrappedSelectedOptions);
return;
}
this.setValue(value, selectedOptions);
};
handlePopupVisibleChange = (popupVisible: boolean) => {
if (!('popupVisible' in this.props)) {
this.setState(state => ({
popupVisible,
inputFocused: popupVisible,
inputValue: popupVisible ? state.inputValue : '',
}));
}
const { onPopupVisibleChange } = this.props;
if (onPopupVisibleChange) {
onPopupVisibleChange(popupVisible);
}
};
handleInputBlur = () => {
this.setState({
inputFocused: false,
});
};
handleInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
const { inputFocused, popupVisible } = this.state;
// Prevent `Trigger` behaviour.
if (inputFocused || popupVisible) {
e.stopPropagation();
if (e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
};
handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// SPACE => https://github.com/ant-design/ant-design/issues/16871
if (e.keyCode === KeyCode.BACKSPACE || e.keyCode === KeyCode.SPACE) {
e.stopPropagation();
}
};
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
this.setState({ inputValue });
};
clearSelection = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
if (!this.state.inputValue) {
this.setValue([]);
this.handlePopupVisibleChange(false);
} else {
this.setState({ inputValue: '' });
}
};
generateFilteredOptions(prefixCls: string | undefined, renderEmpty: RenderEmptyHandler) {
const { showSearch, notFoundContent } = this.props;
const names: FilledFieldNamesType = getFilledFieldNames(this.props);
const {
filter = defaultFilterOption,
render = defaultRenderFilteredOption,
sort = defaultSortFilteredOption,
limit = defaultLimit,
} = showSearch as ShowSearchType;
const { flattenOptions = [], inputValue } = this.state;
// Limit the filter if needed
let filtered: Array<CascaderOptionType[]>;
if (limit > 0) {
filtered = [];
let matchCount = 0;
// Perf optimization to filter items only below the limit
flattenOptions.some(path => {
const match = filter(this.state.inputValue, path, names);