|
|
@ -40,7 +40,12 @@ export interface ShowSearchType { |
|
|
|
prefixCls: string | undefined, |
|
|
|
names: FilledFieldNamesType, |
|
|
|
) => React.ReactNode; |
|
|
|
sort?: (a: CascaderOptionType[], b: CascaderOptionType[], inputValue: string, names: FilledFieldNamesType) => number; |
|
|
|
sort?: ( |
|
|
|
a: CascaderOptionType[], |
|
|
|
b: CascaderOptionType[], |
|
|
|
inputValue: string, |
|
|
|
names: FilledFieldNamesType, |
|
|
|
) => number; |
|
|
|
matchInputWidth?: boolean; |
|
|
|
limit?: number | false; |
|
|
|
} |
|
|
@ -104,14 +109,23 @@ export interface CascaderState { |
|
|
|
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, |
|
|
|
]); |
|
|
|
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) { |
|
|
|
function defaultFilterOption( |
|
|
|
inputValue: string, |
|
|
|
path: CascaderOptionType[], |
|
|
|
names: FilledFieldNamesType, |
|
|
|
) { |
|
|
|
return path.some(option => (option[names.label] as string).indexOf(inputValue) > -1); |
|
|
|
} |
|
|
|
|
|
|
@ -123,14 +137,19 @@ function defaultRenderFilteredOption( |
|
|
|
) { |
|
|
|
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; |
|
|
|
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, |
|
|
|
a: CascaderOptionType[], |
|
|
|
b: CascaderOptionType[], |
|
|
|
inputValue: string, |
|
|
|
names: FilledFieldNamesType, |
|
|
|
) { |
|
|
|
function callback(elem: CascaderOptionType) { |
|
|
|
return (elem[names.label] as string).indexOf(inputValue) > -1; |
|
|
@ -183,8 +202,7 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
inputValue: '', |
|
|
|
inputFocused: false, |
|
|
|
popupVisible: props.popupVisible, |
|
|
|
flattenOptions: |
|
|
|
props.showSearch ? this.flattenTree(props.options, props) : undefined, |
|
|
|
flattenOptions: props.showSearch ? this.flattenTree(props.options, props) : undefined, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
@ -211,7 +229,7 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
return; |
|
|
|
} |
|
|
|
this.setValue(value, selectedOptions); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
handlePopupVisibleChange = (popupVisible: boolean) => { |
|
|
|
if (!('popupVisible' in this.props)) { |
|
|
@ -226,13 +244,13 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
if (onPopupVisibleChange) { |
|
|
|
onPopupVisibleChange(popupVisible); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
handleInputBlur = () => { |
|
|
|
this.setState({ |
|
|
|
inputFocused: false, |
|
|
|
}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
handleInputClick = (e: React.MouseEvent<HTMLInputElement>) => { |
|
|
|
const { inputFocused, popupVisible } = this.state; |
|
|
@ -243,18 +261,18 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
e.nativeEvent.stopImmediatePropagation(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { |
|
|
|
if (e.keyCode === KeyCode.BACKSPACE) { |
|
|
|
e.stopPropagation(); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
const inputValue = e.target.value; |
|
|
|
this.setState({ inputValue }); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
setValue = (value: string[], selectedOptions: CascaderOptionType[] = []) => { |
|
|
|
if (!('value' in this.props)) { |
|
|
@ -264,14 +282,15 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
if (onChange) { |
|
|
|
onChange(value, selectedOptions); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
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; |
|
|
|
const selectedOptions: CascaderOptionType[] = arrayTreeFilter(options, |
|
|
|
const selectedOptions: CascaderOptionType[] = arrayTreeFilter( |
|
|
|
options, |
|
|
|
(o: CascaderOptionType, level: number) => o[names.value] === unwrappedValue[level], |
|
|
|
{ childrenKeyName: names.children }, |
|
|
|
); |
|
|
@ -288,7 +307,7 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
} else { |
|
|
|
this.setState({ inputValue: '' }); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
flattenTree( |
|
|
|
options: CascaderOptionType[], |
|
|
@ -298,19 +317,13 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
const names: FilledFieldNamesType = getFilledFieldNames(props); |
|
|
|
let flattenOptions = [] as CascaderOptionType[][]; |
|
|
|
const childrenName = names.children; |
|
|
|
options.forEach((option) => { |
|
|
|
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, |
|
|
|
), |
|
|
|
); |
|
|
|
flattenOptions = flattenOptions.concat(this.flattenTree(option[childrenName], props, path)); |
|
|
|
} |
|
|
|
}); |
|
|
|
return flattenOptions; |
|
|
@ -334,7 +347,7 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
let matchCount = 0; |
|
|
|
|
|
|
|
// Perf optimization to filter items only below the limit
|
|
|
|
flattenOptions.some((path) => { |
|
|
|
flattenOptions.some(path => { |
|
|
|
const match = filter(this.state.inputValue, path, names); |
|
|
|
if (match) { |
|
|
|
filtered.push(path); |
|
|
@ -345,9 +358,9 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
} else { |
|
|
|
warning( |
|
|
|
typeof limit !== 'number', |
|
|
|
'\'limit\' of showSearch in Cascader should be positive number or false.', |
|
|
|
"'limit' of showSearch in Cascader should be positive number or false.", |
|
|
|
); |
|
|
|
filtered = flattenOptions.filter((path) => filter(this.state.inputValue, path, names)); |
|
|
|
filtered = flattenOptions.filter(path => filter(this.state.inputValue, path, names)); |
|
|
|
} |
|
|
|
|
|
|
|
filtered.sort((a, b) => sort(a, b, inputValue, names)); |
|
|
@ -363,7 +376,9 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
} as CascaderOptionType; |
|
|
|
}); |
|
|
|
} |
|
|
|
return [{ [names.label]: notFoundContent, [names.value]: 'ANT_CASCADER_NOT_FOUND', disabled: true }]; |
|
|
|
return [ |
|
|
|
{ [names.label]: notFoundContent, [names.value]: 'ANT_CASCADER_NOT_FOUND', disabled: true }, |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
focus() { |
|
|
@ -376,13 +391,23 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
|
|
|
|
saveInput = (node: Input) => { |
|
|
|
this.input = node; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
renderCascader = ({ getPopupContainer: getContextPopupContainer }: ConfigProviderProps) => { |
|
|
|
const { props, state } = this; |
|
|
|
const { |
|
|
|
prefixCls, inputPrefixCls, children, placeholder, size, disabled, |
|
|
|
className, style, allowClear, showSearch = false, suffixIcon, ...otherProps |
|
|
|
prefixCls, |
|
|
|
inputPrefixCls, |
|
|
|
children, |
|
|
|
placeholder, |
|
|
|
size, |
|
|
|
disabled, |
|
|
|
className, |
|
|
|
style, |
|
|
|
allowClear, |
|
|
|
showSearch = false, |
|
|
|
suffixIcon, |
|
|
|
...otherProps |
|
|
|
} = props; |
|
|
|
const { value, inputFocused } = state; |
|
|
|
|
|
|
@ -390,26 +415,26 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
[`${inputPrefixCls}-lg`]: size === 'large', |
|
|
|
[`${inputPrefixCls}-sm`]: size === 'small', |
|
|
|
}); |
|
|
|
const clearIcon = (allowClear && !disabled && value.length > 0) || state.inputValue ? ( |
|
|
|
<Icon |
|
|
|
type="close-circle" |
|
|
|
theme="filled" |
|
|
|
className={`${prefixCls}-picker-clear`} |
|
|
|
onClick={this.clearSelection} |
|
|
|
/> |
|
|
|
) : null; |
|
|
|
const clearIcon = |
|
|
|
(allowClear && !disabled && value.length > 0) || state.inputValue ? ( |
|
|
|
<Icon |
|
|
|
type="close-circle" |
|
|
|
theme="filled" |
|
|
|
className={`${prefixCls}-picker-clear`} |
|
|
|
onClick={this.clearSelection} |
|
|
|
/> |
|
|
|
) : null; |
|
|
|
const arrowCls = classNames({ |
|
|
|
[`${prefixCls}-picker-arrow`]: true, |
|
|
|
[`${prefixCls}-picker-arrow-expand`]: state.popupVisible, |
|
|
|
}); |
|
|
|
const pickerCls = classNames( |
|
|
|
className, `${prefixCls}-picker`, { |
|
|
|
[`${prefixCls}-picker-with-value`]: state.inputValue, |
|
|
|
[`${prefixCls}-picker-disabled`]: disabled, |
|
|
|
[`${prefixCls}-picker-${size}`]: !!size, |
|
|
|
[`${prefixCls}-picker-show-search`]: !!showSearch, |
|
|
|
[`${prefixCls}-picker-focused`]: inputFocused, |
|
|
|
}); |
|
|
|
const pickerCls = classNames(className, `${prefixCls}-picker`, { |
|
|
|
[`${prefixCls}-picker-with-value`]: state.inputValue, |
|
|
|
[`${prefixCls}-picker-disabled`]: disabled, |
|
|
|
[`${prefixCls}-picker-${size}`]: !!size, |
|
|
|
[`${prefixCls}-picker-show-search`]: !!showSearch, |
|
|
|
[`${prefixCls}-picker-focused`]: inputFocused, |
|
|
|
}); |
|
|
|
|
|
|
|
// Fix bug of https://github.com/facebook/react/pull/5004
|
|
|
|
// and https://fb.me/react-unknown-prop
|
|
|
@ -445,39 +470,34 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
this.cachedOptions = options; |
|
|
|
} |
|
|
|
|
|
|
|
const dropdownMenuColumnStyle: { width?: number, height?: string } = {}; |
|
|
|
const isNotFound = (options || []).length === 1 && options[0].value === 'ANT_CASCADER_NOT_FOUND'; |
|
|
|
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`
|
|
|
|
const resultListMatchInputWidth = (showSearch as ShowSearchType).matchInputWidth === false ? false : true; |
|
|
|
const resultListMatchInputWidth = |
|
|
|
(showSearch as ShowSearchType).matchInputWidth === false ? false : true; |
|
|
|
if (resultListMatchInputWidth && state.inputValue && this.input) { |
|
|
|
dropdownMenuColumnStyle.width = this.input.input.offsetWidth; |
|
|
|
} |
|
|
|
|
|
|
|
const inputIcon = suffixIcon && ( |
|
|
|
React.isValidElement<{ className?: string }>(suffixIcon) |
|
|
|
? React.cloneElement( |
|
|
|
suffixIcon, |
|
|
|
{ |
|
|
|
className: classNames({ |
|
|
|
[suffixIcon.props.className!]: suffixIcon.props.className, |
|
|
|
[`${prefixCls}-picker-arrow`]: true, |
|
|
|
}), |
|
|
|
}, |
|
|
|
) : <span className={`${prefixCls}-picker-arrow`}>{suffixIcon}</span>) || ( |
|
|
|
<Icon type="down" className={arrowCls} /> |
|
|
|
); |
|
|
|
const inputIcon = (suffixIcon && |
|
|
|
(React.isValidElement<{ className?: string }>(suffixIcon) ? ( |
|
|
|
React.cloneElement(suffixIcon, { |
|
|
|
className: classNames({ |
|
|
|
[suffixIcon.props.className!]: suffixIcon.props.className, |
|
|
|
[`${prefixCls}-picker-arrow`]: true, |
|
|
|
}), |
|
|
|
}) |
|
|
|
) : ( |
|
|
|
<span className={`${prefixCls}-picker-arrow`}>{suffixIcon}</span> |
|
|
|
))) || <Icon type="down" className={arrowCls} />; |
|
|
|
|
|
|
|
const input = children || ( |
|
|
|
<span |
|
|
|
style={style} |
|
|
|
className={pickerCls} |
|
|
|
> |
|
|
|
<span className={`${prefixCls}-picker-label`}> |
|
|
|
{this.getLabel()} |
|
|
|
</span> |
|
|
|
<span style={style} className={pickerCls}> |
|
|
|
<span className={`${prefixCls}-picker-label`}>{this.getLabel()}</span> |
|
|
|
<Input |
|
|
|
{...inputProps} |
|
|
|
ref={this.saveInput} |
|
|
@ -498,9 +518,7 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
</span> |
|
|
|
); |
|
|
|
|
|
|
|
const expandIcon = ( |
|
|
|
<Icon type="right" /> |
|
|
|
); |
|
|
|
const expandIcon = <Icon type="right" />; |
|
|
|
|
|
|
|
const loadingIcon = ( |
|
|
|
<span className={`${prefixCls}-menu-item-loading-icon`}> |
|
|
@ -527,13 +545,9 @@ export default class Cascader extends React.Component<CascaderProps, CascaderSta |
|
|
|
{input} |
|
|
|
</RcCascader> |
|
|
|
); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
render() { |
|
|
|
return ( |
|
|
|
<ConfigConsumer> |
|
|
|
{this.renderCascader} |
|
|
|
</ConfigConsumer> |
|
|
|
); |
|
|
|
return <ConfigConsumer>{this.renderCascader}</ConfigConsumer>; |
|
|
|
} |
|
|
|
} |
|
|
|