ant-design/components/cascader/index.tsx

529 lines
16 KiB
TypeScript
Raw Normal View History

import * as React from 'react';
import RcCascader from 'rc-cascader';
2015-12-29 11:46:13 +08:00
import arrayTreeFilter from 'array-tree-filter';
2015-12-29 18:31:48 +08:00
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 warning from '../_util/warning';
2015-12-29 11:46:13 +08:00
export interface CascaderOptionType {
value?: string;
label?: React.ReactNode;
2016-07-13 11:14:24 +08:00
disabled?: boolean;
children?: Array<CascaderOptionType>;
2018-08-13 11:44:25 +08:00
[key: string]: any;
}
2018-07-03 19:54:33 +08:00
export interface FieldNamesType {
value?: string;
label?: string;
children?: string;
}
2018-07-03 19:54:33 +08:00
export interface FilledFieldNamesType {
value: string;
label: string;
children: string;
}
export type CascaderExpandTrigger = 'click' | 'hover';
export interface ShowSearchType {
2018-07-03 19:54:33 +08:00
filter?: (inputValue: string, path: CascaderOptionType[], names: FilledFieldNamesType) => boolean;
render?: (
inputValue: string,
path: CascaderOptionType[],
prefixCls: string | undefined,
2018-07-03 19:54:33 +08:00
names: FilledFieldNamesType,
) => React.ReactNode;
2018-07-03 19:54:33 +08:00
sort?: (a: CascaderOptionType[], b: CascaderOptionType[], inputValue: string, names: FilledFieldNamesType) => number;
matchInputWidth?: boolean;
limit?: number | false;
}
2016-07-07 20:25:03 +08:00
export interface CascaderProps {
2016-07-13 11:14:24 +08:00
/** 可选项数据源 */
options: CascaderOptionType[];
2016-07-13 11:14:24 +08:00
/** 默认的选中项 */
2017-11-21 21:52:40 +08:00
defaultValue?: string[];
2016-07-13 11:14:24 +08:00
/** 指定选中项 */
2017-11-21 21:52:40 +08:00
value?: string[];
2016-07-13 11:14:24 +08:00
/** 选择完成后的回调 */
onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
2016-07-13 11:14:24 +08:00
/** 选择后展示的渲染函数 */
displayRender?: (label: string[], selectedOptions?: CascaderOptionType[]) => React.ReactNode;
2016-07-13 11:14:24 +08:00
/** 自定义样式 */
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
/** 自定义浮层类名 */
popupClassName?: string;
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
2016-07-13 11:14:24 +08:00
popupPlacement?: string;
/** 输入框占位文本*/
2016-07-13 11:14:24 +08:00
placeholder?: string;
/** 输入框大小,可选 `large` `default` `small` */
2016-07-13 11:14:24 +08:00
size?: string;
/** 禁用*/
2016-07-13 11:14:24 +08:00
disabled?: boolean;
/** 是否支持清除*/
2016-07-13 11:14:24 +08:00
allowClear?: boolean;
showSearch?: boolean | ShowSearchType;
notFoundContent?: React.ReactNode;
loadData?: (selectedOptions?: CascaderOptionType[]) => void;
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
2016-07-13 11:14:24 +08:00
expandTrigger?: CascaderExpandTrigger;
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
2016-07-13 11:14:24 +08:00
changeOnSelect?: boolean;
/** 浮层可见变化时回调 */
2016-07-13 11:14:24 +08:00
onPopupVisibleChange?: (popupVisible: boolean) => void;
prefixCls?: string;
inputPrefixCls?: string;
2017-03-21 16:35:31 +08:00
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
2017-11-21 21:52:40 +08:00
popupVisible?: boolean;
/** use this after antd@3.7.0 */
2018-07-03 19:54:33 +08:00
fieldNames?: FieldNamesType;
/** typo props name before antd@3.7.0 */
filedNames?: FieldNamesType;
2018-09-16 19:36:19 +08:00
suffixIcon?: React.ReactNode;
}
2017-11-21 21:52:40 +08:00
export interface CascaderState {
inputFocused: boolean;
inputValue: string;
value: string[];
popupVisible: boolean | undefined;
flattenOptions: CascaderOptionType[][] | undefined;
2017-11-21 21:52:40 +08:00
}
// We limit the filtered item count by default
const defaultLimit = 50;
2017-11-21 21:52:40 +08:00
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,
]);
}
2018-07-03 19:54:33 +08:00
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,
2018-07-03 19:54:33 +08:00
names: FilledFieldNamesType,
) {
return path.map((option, index) => {
const label = option[names.label];
2017-11-21 21:52:40 +08:00
const node = (label as string).indexOf(inputValue) > -1 ?
highlightKeyword(label as string, inputValue, prefixCls) : label;
return index === 0 ? node : [' / ', node];
});
}
function defaultSortFilteredOption(
2018-07-03 19:54:33 +08:00
a: CascaderOptionType[], b: CascaderOptionType[], inputValue: string, names: FilledFieldNamesType,
) {
2017-11-21 21:52:40 +08:00
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) || {};
2018-07-03 19:54:33 +08:00
const names: FilledFieldNamesType = {
children: fieldNames.children || 'children',
label: fieldNames.label || 'label',
value: fieldNames.value || 'value',
};
return names;
}
2017-11-21 21:52:40 +08:00
const defaultDisplayRender = (label: string[]) => label.join(' / ');
2016-10-24 12:04:26 +08:00
2017-11-21 21:52:40 +08:00
export default class Cascader extends React.Component<CascaderProps, CascaderState> {
static defaultProps = {
prefixCls: 'ant-cascader',
inputPrefixCls: 'ant-input',
2016-04-01 13:51:26 +08:00
placeholder: 'Please select',
transitionName: 'slide-up',
popupPlacement: 'bottomLeft',
options: [],
disabled: false,
allowClear: true,
notFoundContent: 'Not Found',
2016-07-13 11:14:24 +08:00
};
cachedOptions: CascaderOptionType[];
2017-09-17 15:48:44 +08:00
private input: Input;
2017-11-21 21:52:40 +08:00
constructor(props: CascaderProps) {
2015-12-29 11:46:13 +08:00
super(props);
this.state = {
value: props.value || props.defaultValue || [],
inputValue: '',
inputFocused: false,
popupVisible: props.popupVisible,
flattenOptions:
props.showSearch ? this.flattenTree(props.options, props) : undefined,
2015-12-29 11:46:13 +08:00
};
}
2017-11-21 21:52:40 +08:00
componentWillReceiveProps(nextProps: CascaderProps) {
2015-12-29 21:18:27 +08:00
if ('value' in nextProps) {
2016-01-06 11:45:47 +08:00
this.setState({ value: nextProps.value || [] });
2015-12-29 21:18:27 +08:00
}
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),
});
}
2015-12-29 21:18:27 +08:00
}
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);
2015-12-29 21:18:27 +08:00
}
2017-11-21 21:52:40 +08:00
handlePopupVisibleChange = (popupVisible: boolean) => {
if (!('popupVisible' in this.props)) {
this.setState({
popupVisible,
inputFocused: popupVisible,
inputValue: popupVisible ? this.state.inputValue : '',
});
}
2016-10-24 12:04:26 +08:00
const onPopupVisibleChange = this.props.onPopupVisibleChange;
if (onPopupVisibleChange) {
onPopupVisibleChange(popupVisible);
}
2015-12-29 21:18:27 +08:00
}
handleInputBlur = () => {
this.setState({
inputFocused: false,
});
}
2017-11-21 21:52:40 +08:00
handleInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
const { inputFocused, popupVisible } = this.state;
// Prevent `Trigger` behaviour.
if (inputFocused || popupVisible) {
e.stopPropagation();
2018-05-25 19:31:34 +08:00
if (e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
}
2017-11-21 21:52:40 +08:00
handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.keyCode === KeyCode.BACKSPACE) {
e.stopPropagation();
}
}
2017-11-21 21:52:40 +08:00
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
this.setState({ inputValue });
}
setValue = (value: string[], selectedOptions: CascaderOptionType[] = []) => {
2015-12-29 21:18:27 +08:00
if (!('value' in this.props)) {
this.setState({ value });
}
2016-10-24 12:04:26 +08:00
const onChange = this.props.onChange;
if (onChange) {
onChange(value, selectedOptions);
}
2015-12-29 11:46:13 +08:00
}
2015-12-29 11:46:13 +08:00
getLabel() {
const { options, displayRender = defaultDisplayRender as Function } = this.props;
const names = getFilledFieldNames(this.props);
const value = this.state.value;
const unwrappedValue = Array.isArray(value[0]) ? value[0] : value;
2017-11-21 21:52:40 +08:00
const selectedOptions: CascaderOptionType[] = arrayTreeFilter(options,
(o: CascaderOptionType, level: number) => o[names.value] === unwrappedValue[level],
{ childrenKeyName: names.children },
2017-11-21 21:52:40 +08:00
);
const label = selectedOptions.map(o => o[names.label]);
return displayRender(label, selectedOptions);
2015-12-29 11:46:13 +08:00
}
2017-11-21 21:52:40 +08:00
clearSelection = (e: React.MouseEvent<HTMLElement>) => {
2015-12-29 21:18:27 +08:00
e.preventDefault();
2015-12-29 22:34:23 +08:00
e.stopPropagation();
if (!this.state.inputValue) {
this.setValue([]);
this.handlePopupVisibleChange(false);
} else {
this.setState({ inputValue: '' });
}
}
flattenTree(
options: CascaderOptionType[],
props: CascaderProps,
ancestor: CascaderOptionType[] = [],
) {
const names: FilledFieldNamesType = getFilledFieldNames(props);
let flattenOptions = [] as CascaderOptionType[][];
2018-11-10 21:40:21 +08:00
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(
this.flattenTree(
option[childrenName],
props,
path,
),
);
}
});
return flattenOptions;
}
2017-11-21 21:52:40 +08:00
generateFilteredOptions(prefixCls: string | undefined) {
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);
if (match) {
filtered.push(path);
matchCount += 1;
}
return matchCount >= limit;
});
} else {
warning(
typeof limit !== 'number',
'\'limit\' of showSearch in Cascader should be positive number or false.',
);
filtered = flattenOptions.filter((path) => filter(this.state.inputValue, path, names));
}
filtered.sort((a, b) => sort(a, b, inputValue, names));
if (filtered.length > 0) {
return filtered.map((path: CascaderOptionType[]) => {
return {
__IS_FILTERED_OPTION: true,
path,
[names.label]: render(inputValue, path, prefixCls, names),
[names.value]: path.map((o: CascaderOptionType) => o[names.value]),
disabled: path.some((o: CascaderOptionType) => !!o.disabled),
2017-11-21 21:52:40 +08:00
} as CascaderOptionType;
});
}
return [{ [names.label]: notFoundContent, [names.value]: 'ANT_CASCADER_NOT_FOUND', disabled: true }];
2015-12-29 21:18:27 +08:00
}
focus() {
this.input.focus();
}
blur() {
this.input.blur();
}
2017-11-21 21:52:40 +08:00
saveInput = (node: Input) => {
this.input = node;
}
2015-12-29 11:46:13 +08:00
render() {
const { props, state } = this;
2016-12-19 15:19:15 +08:00
const {
prefixCls, inputPrefixCls, children, placeholder, size, disabled,
2018-09-16 19:36:19 +08:00
className, style, allowClear, showSearch = false, suffixIcon, ...otherProps
2016-12-19 15:19:15 +08:00
} = props;
const { value, inputFocused } = state;
2016-07-07 15:13:01 +08:00
2015-12-29 18:31:48 +08:00
const sizeCls = classNames({
2016-09-14 16:59:45 +08:00
[`${inputPrefixCls}-lg`]: size === 'large',
[`${inputPrefixCls}-sm`]: size === 'small',
2015-12-29 18:31:48 +08:00
});
const clearIcon = (allowClear && !disabled && value.length > 0) || state.inputValue ? (
<Icon
type="close-circle"
theme="filled"
2015-12-29 21:18:27 +08:00
className={`${prefixCls}-picker-clear`}
onClick={this.clearSelection}
/>
) : null;
2015-12-29 22:34:23 +08:00
const arrowCls = classNames({
[`${prefixCls}-picker-arrow`]: true,
[`${prefixCls}-picker-arrow-expand`]: state.popupVisible,
2015-12-29 22:34:23 +08:00
});
const pickerCls = classNames(
className, `${prefixCls}-picker`, {
2018-08-13 11:44:25 +08:00
[`${prefixCls}-picker-with-value`]: state.inputValue,
[`${prefixCls}-picker-disabled`]: disabled,
[`${prefixCls}-picker-${size}`]: !!size,
[`${prefixCls}-picker-show-search`]: !!showSearch,
[`${prefixCls}-picker-focused`]: inputFocused,
});
2016-03-03 14:57:26 +08:00
// Fix bug of https://github.com/facebook/react/pull/5004
// and https://fb.me/react-unknown-prop
const inputProps = omit(otherProps, [
'onChange',
'options',
'popupPlacement',
'transitionName',
'displayRender',
'onPopupVisibleChange',
'changeOnSelect',
'expandTrigger',
2016-07-16 15:50:27 +08:00
'popupVisible',
'getPopupContainer',
'loadData',
'popupClassName',
'filterOption',
'renderFilteredOption',
'sortFilteredOption',
'notFoundContent',
2018-07-03 19:54:33 +08:00
'fieldNames',
'filedNames', // For old compatibility
]);
2016-03-03 14:57:26 +08:00
let options = props.options;
if (state.inputValue) {
options = this.generateFilteredOptions(prefixCls);
}
// Dropdown menu should keep previous status until it is fully closed.
if (!state.popupVisible) {
options = this.cachedOptions;
} else {
this.cachedOptions = options;
}
2016-10-24 12:04:26 +08:00
const dropdownMenuColumnStyle: { width?: number, height?: string } = {};
const isNotFound = (options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND';
if (isNotFound) {
dropdownMenuColumnStyle.height = 'auto'; // Height of one row.
}
// The default value of `matchInputWidth` is `true`
2016-12-19 15:19:15 +08:00
const resultListMatchInputWidth = (showSearch as ShowSearchType).matchInputWidth === false ? false : true;
if (resultListMatchInputWidth && state.inputValue && this.input) {
dropdownMenuColumnStyle.width = this.input.input.offsetWidth;
}
2018-09-16 19:36:19 +08:00
const inputIcon = suffixIcon && (
React.isValidElement<{ className?: string }>(suffixIcon)
2018-09-14 22:22:43 +08:00
? React.cloneElement(
2018-09-16 19:36:19 +08:00
suffixIcon,
2018-09-14 22:22:43 +08:00
{
className: classNames({
2018-09-16 19:36:19 +08:00
[suffixIcon.props.className!]: suffixIcon.props.className,
2018-09-14 22:22:43 +08:00
[`${prefixCls}-picker-arrow`]: true,
}),
},
2018-09-16 19:36:19 +08:00
) : <span className={`${prefixCls}-picker-arrow`}>{suffixIcon}</span>) || (
2018-09-14 22:22:43 +08:00
<Icon type="down" className={arrowCls} />
);
const input = children || (
<span
style={style}
className={pickerCls}
>
<span className={`${prefixCls}-picker-label`}>
{this.getLabel()}
</span>
<Input
{...inputProps}
ref={this.saveInput}
prefixCls={inputPrefixCls}
2016-12-19 15:19:15 +08:00
placeholder={value && value.length > 0 ? undefined : placeholder}
className={`${prefixCls}-input ${sizeCls}`}
value={state.inputValue}
disabled={disabled}
readOnly={!showSearch}
autoComplete="off"
onClick={showSearch ? this.handleInputClick : undefined}
onBlur={showSearch ? this.handleInputBlur : undefined}
onKeyDown={this.handleKeyDown}
onChange={showSearch ? this.handleInputChange : undefined}
/>
{clearIcon}
2018-09-14 22:22:43 +08:00
{inputIcon}
</span>
);
2018-08-13 11:44:25 +08:00
const expandIcon = (
2018-08-23 22:15:25 +08:00
<Icon type="right" />
);
const loadingIcon = (
<span className={`${prefixCls}-menu-item-loading-icon`}>
<Icon type="redo" spin />
</span>
2018-08-13 11:44:25 +08:00
);
2018-09-14 22:22:43 +08:00
const rest = omit(props, ['inputIcon', 'expandIcon', 'loadingIcon']);
2015-12-29 11:46:13 +08:00
return (
2016-10-18 18:10:41 +08:00
<RcCascader
2018-09-14 22:22:43 +08:00
{...rest}
options={options}
value={value}
popupVisible={state.popupVisible}
2015-12-29 21:18:27 +08:00
onPopupVisibleChange={this.handlePopupVisibleChange}
onChange={this.handleChange}
dropdownMenuColumnStyle={dropdownMenuColumnStyle}
2018-08-13 11:44:25 +08:00
expandIcon={expandIcon}
2018-08-23 22:15:25 +08:00
loadingIcon={loadingIcon}
>
{input}
</RcCascader>
2015-12-29 11:46:13 +08:00
);
}
}