|
|
|
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 Input from '../input';
|
|
|
|
import Icon from '../icon';
|
|
|
|
import { ConfigConsumer, ConfigProviderProps } from '../config-provider';
|
|
|
|
import warning from '../_util/warning';
|
|
|
|
|
|
|
|
export interface CascaderOptionType {
|
|
|
|
value?: string;
|
|
|
|
label?: React.ReactNode;
|
|
|
|
disabled?: 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultDisplayRender = (label: string[]) => label.join(' / ');
|
|
|
|
|
|
|
|
export default class Cascader extends React.Component<CascaderProps, CascaderState> {
|
|
|
|
static defaultProps = {
|
|
|
|
prefixCls: 'ant-cascader',
|
|
|
|
inputPrefixCls: 'ant-input',
|
|
|
|
placeholder: 'Please select',
|
|
|
|
transitionName: 'slide-up',
|
|
|
|
popupPlacement: 'bottomLeft',
|
|
|
|
options: [],
|
|
|
|
disabled: false,
|
|
|
|
allowClear: true,
|
|
|
|
notFoundContent: 'Not Found',
|
|
|
|
};
|
|
|
|
|
|
|
|
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 ? this.flattenTree(props.options, props) : undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps(nextProps: CascaderProps) {
|
|
|
|
if ('value' in nextProps) {
|
|
|
|
this.setState({ value: nextProps.value || [] });
|
|
|
|
}
|
|
|
|
if ('popupVisible' in nextProps) {
|
|
|
|
this.setState({ popupVisible: nextProps.popupVisible });
|
|
|
|
}
|
|
|
|
if (nextProps.showSearch && this.props.options !== nextProps.options) {
|
|
|
|
this.setState({
|
|
|
|
flattenOptions: this.flattenTree(nextProps.options, nextProps),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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({
|
|
|
|
popupVisible,
|
|
|
|
inputFocused: popupVisible,
|
|
|
|
inputValue: popupVisible ? this.state.inputValue : '',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const onPopupVisibleChange = this.props.onPopupVisibleChange;
|
|
|
|
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) {
|
|
|
|
|