@ -15,6 +15,14 @@ export interface CascaderOptionType {
}
export type CascaderExpandTrigger = 'click' | 'hover'
export interface ShowSearchType {
filter ? : ( inputValue : string , path : CascaderOptionType [ ] ) = > boolean ;
render ? : ( inputValue : string , path : CascaderOptionType [ ] ) = > React . ReactNode ;
sort ? : ( a : CascaderOptionType [ ] , b : CascaderOptionType [ ] , inputValue : string ) = > number ;
matchInputWidth? : boolean ;
}
export interface CascaderProps {
/** 可选项数据源 */
options : Array < CascaderOptionType > ;
@ -42,6 +50,8 @@ export interface CascaderProps {
disabled? : boolean ;
/** 是否支持清除*/
allowClear? : boolean ;
showSearch? : boolean | ShowSearchType ;
notFoundContent? : React.ReactNode ;
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
expandTrigger? : CascaderExpandTrigger ;
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
@ -50,6 +60,33 @@ export interface CascaderProps {
onPopupVisibleChange ? : ( popupVisible : boolean ) = > void ;
}
function highlightKeyword ( str : string , keyword : string ) {
return str . split ( keyword )
. map ( ( node : string , index : number ) = > index === 0 ? node : [
< span className = "ant-cascader-menu-item-keyword" key = "seperator" > { keyword } < / span > ,
node ,
] ) ;
}
function defaultFilterOption ( inputValue , path ) {
return path . some ( option = > option . label . indexOf ( inputValue ) > - 1 ) ;
}
function defaultRenderFilteredOption ( inputValue , path ) {
return path . map ( ( { label } , index ) = > {
const node = label . indexOf ( inputValue ) > - 1 ? highlightKeyword ( label , inputValue ) : label ;
return index === 0 ? node : [ ' / ' , node ] ;
} ) ;
}
function defaultSortFilteredOption ( a , b , inputValue ) {
function callback ( elem ) {
return elem . label . indexOf ( inputValue ) > - 1 ;
}
return a . findIndex ( callback ) - b . findIndex ( callback ) ;
}
export default class Cascader extends React . Component < CascaderProps , any > {
static defaultProps = {
prefixCls : 'ant-cascader' ,
@ -61,20 +98,27 @@ export default class Cascader extends React.Component<CascaderProps, any> {
displayRender : label = > label . join ( ' / ' ) ,
disabled : false ,
allowClear : true ,
showSearch : false ,
notFoundContent : 'Not Found' ,
onPopupVisibleChange() { } ,
} ;
cachedOptions : CascaderOptionType [ ] ;
refs : {
[ key : string ] : any ;
input : {
refs : { input : HTMLElement }
} ;
} ;
constructor ( props ) {
super ( props ) ;
let value ;
if ( 'value' in props ) {
value = props . value ;
} else if ( 'defaultValue' in props ) {
value = props . defaultValue ;
}
this . state = {
value : value || [ ] ,
value : props.value || props . defautValue || [ ] ,
inputValue : '' ,
inputFocused : false ,
popupVisible : false ,
flattenOptions : props.showSearch && this . flattenTree ( props . options , props . changeOnSelect ) ,
} ;
}
@ -82,17 +126,45 @@ export default class Cascader extends React.Component<CascaderProps, any> {
if ( 'value' in nextProps ) {
this . setState ( { value : nextProps.value || [ ] } ) ;
}
if ( nextProps . showSearch && this . props . options !== nextProps . options ) {
this . setState ( { flattenOptions : this.flattenTree ( nextProps . options , nextProps . changeOnSelect ) } ) ;
}
}
handleChange = ( value , selectedOptions ) = > {
this . setValue ( value , selectedOptions ) ;
const unwrappedValue = Array . isArray ( value [ 0 ] ) ? value [ 0 ] : value ;
this . setState ( { inputValue : '' } ) ;
this . setValue ( unwrappedValue , selectedOptions ) ;
}
handlePopupVisibleChange = ( popupVisible ) = > {
this . setState ( { popupVisible } ) ;
this . setState ( {
popupVisible ,
inputFocused : popupVisible ,
} ) ;
this . props . onPopupVisibleChange ( popupVisible ) ;
}
handleInputBlur = ( ) = > {
this . setState ( {
inputFocused : false ,
} ) ;
}
handleInputClick = ( e ) = > {
const { inputFocused , popupVisible } = this . state ;
// Prevent `Trigger` behaviour.
if ( inputFocused || popupVisible ) {
e . stopPropagation ( ) ;
e . nativeEvent . stopImmediatePropagation ( ) ;
}
}
handleInputChange = ( e ) = > {
const inputValue = e . target . value ;
this . setState ( { inputValue } ) ;
}
setValue = ( value , selectedOptions = [ ] ) = > {
if ( ! ( 'value' in this . props ) ) {
this . setState ( { value } ) ;
@ -102,7 +174,9 @@ export default class Cascader extends React.Component<CascaderProps, any> {
getLabel() {
const { options , displayRender } = this . props ;
const selectedOptions = arrayTreeFilter ( options , ( o , level ) = > o . value === this . state . value [ level ] ) ;
const value = this . state . value ;
const unwrappedValue = Array . isArray ( value [ 0 ] ) ? value [ 0 ] : value ;
const selectedOptions = arrayTreeFilter ( options , ( o , level ) = > o . value === unwrappedValue [ level ] ) ;
const label = selectedOptions . map ( o = > o . label ) ;
return displayRender ( label , selectedOptions ) ;
}
@ -110,28 +184,71 @@ export default class Cascader extends React.Component<CascaderProps, any> {
clearSelection = ( e ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
this . setValue ( [ ] ) ;
this . setState ( { popupVisible : false } ) ;
if ( ! this . state . inputValue ) {
this . setValue ( [ ] ) ;
this . setState ( { popupVisible : false } ) ;
} else {
this . setState ( { inputValue : '' } ) ;
}
}
flattenTree ( options , changeOnSelect , ancestor = [ ] ) {
let flattenOptions = [ ] ;
options . forEach ( ( option ) = > {
const path = ancestor . concat ( option ) ;
if ( changeOnSelect || ! option . children ) {
flattenOptions . push ( path ) ;
}
if ( option . children ) {
flattenOptions = flattenOptions . concat ( this . flattenTree ( option . children , changeOnSelect , path ) ) ;
}
} ) ;
return flattenOptions ;
}
generateFilteredOptions() {
const { showSearch , notFoundContent } = this . props ;
const {
filter = defaultFilterOption ,
render = defaultRenderFilteredOption ,
sort = defaultSortFilteredOption ,
} = showSearch as ShowSearchType ;
const { flattenOptions , inputValue } = this . state ;
const filtered = flattenOptions . filter ( ( path ) = > filter ( this . state . inputValue , path ) )
. sort ( ( a , b ) = > sort ( a , b , inputValue ) ) ;
if ( filtered . length > 0 ) {
return filtered . map ( ( path ) = > {
return {
label : render ( inputValue , path ) ,
value : path.map ( o = > o . value ) ,
} ;
} ) ;
}
return [ { label : notFoundContent , value : 'ANT_CASCADER_NOT_FOUND' , disabled : true } ] ;
}
render() {
const props = this . props ;
const state = this . state ;
const [ { prefixCls , children , placeholder , size , disabled ,
className , style , allowClear } , otherProps ] = splitObject ( props ,
[ 'prefixCls' , 'children' , 'placeholder' , 'size' , 'disabled' , 'className' , 'style' , 'allowClear' ] ) ;
className , style , allowClear , showSearch } , otherProps ] = splitObject ( props ,
[ 'prefixCls' , 'children' , 'placeholder' , 'size' , 'disabled' , 'className' ,
'style' , 'allowClear' , 'showSearch' ] ) ;
const value = state . value ;
const sizeCls = classNames ( {
'ant-input-lg' : size === 'large' ,
'ant-input-sm' : size === 'small' ,
} ) ;
const clearIcon = ( allowClear && ! disabled && this . state . value . length > 0 ) ?
const clearIcon = ( allowClear && ! disabled && value . length > 0 ) || state . inputValue ?
< Icon type = "cross-circle"
className = { ` ${ prefixCls } -picker-clear ` }
onClick = { this . clearSelection }
/ > : n u l l ;
const arrowCls = classNames ( {
[ ` ${ prefixCls } -picker-arrow ` ] : true ,
[ ` ${ prefixCls } -picker-arrow-expand ` ] : this . state . popupVisible ,
[ ` ${ prefixCls } -picker-arrow-expand ` ] : state . popupVisible ,
} ) ;
const pickerCls = classNames ( {
[ className ] : ! ! className ,
@ -154,14 +271,44 @@ export default class Cascader extends React.Component<CascaderProps, any> {
'getPopupContainer' ,
'loadData' ,
'popupClassName' ,
'filterOption' ,
'renderFilteredOption' ,
'sortFilteredOption' ,
'notFoundContent' ,
] ) ;
let options = props . options ;
if ( state . inputValue ) {
options = this . generateFilteredOptions ( ) ;
}
// Dropdown menu should keep previous status until it is fully closed.
if ( ! state . popupVisible ) {
options = this . cachedOptions ;
} else {
this . cachedOptions = options ;
}
const dropdownMenuColumnStyle = {
width : undefined ,
height : undefined ,
} ;
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 . matchInputWidth === false ? false : true ;
if ( resultListMatchInputWidth && state . inputValue && this . refs . input ) {
dropdownMenuColumnStyle . width = this . refs . input . refs . input . offsetWidth ;
}
return (
< RcCascader { ...props }
value = { this . state . value }
popupVisible = { this . state . popupVisible }
options = { options }
value = { value }
popupVisible = { state . popupVisible }
onPopupVisibleChange = { this . handlePopupVisibleChange }
onChange = { this . handleChange }
dropdownMenuColumnStyle = { dropdownMenuColumnStyle }
>
{ children ||
< span
@ -169,11 +316,15 @@ export default class Cascader extends React.Component<CascaderProps, any> {
className = { pickerCls }
>
< Input { ...inputProps }
placeholder = { this . state . value && this . state . value . length > 0 ? null : placeholder }
ref = "input"
placeholder = { value && value . length > 0 ? null : placeholder }
className = { ` ${ prefixCls } -input ${ sizeCls } ` }
value = ""
value = { state . inputValue }
disabled = { disabled }
readOnly
readOnly = { ! showSearch }
onClick = { showSearch ? this . handleInputClick : null }
onBlur = { showSearch ? this . handleInputBlur : null }
onChange = { showSearch ? this . handleInputChange : null }
/ >
< span className = { ` ${ prefixCls } -picker-label ` } > { this . getLabel ( ) } < / span >
{ clearIcon }