feat: cascader support limit
parent
843b074fed
commit
3a49503baa
|
@ -4,6 +4,9 @@ import KeyCode from '../../_util/KeyCode'
|
||||||
import Cascader from '..'
|
import Cascader from '..'
|
||||||
import focusTest from '../../../tests/shared/focusTest'
|
import focusTest from '../../../tests/shared/focusTest'
|
||||||
|
|
||||||
|
function $$ (className) {
|
||||||
|
return document.body.querySelectorAll(className)
|
||||||
|
}
|
||||||
const options = [{
|
const options = [{
|
||||||
value: 'zhejiang',
|
value: 'zhejiang',
|
||||||
label: 'Zhejiang',
|
label: 'Zhejiang',
|
||||||
|
@ -28,6 +31,10 @@ const options = [{
|
||||||
}],
|
}],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
function filter (inputValue, path) {
|
||||||
|
return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1)
|
||||||
|
}
|
||||||
|
|
||||||
describe('Cascader', () => {
|
describe('Cascader', () => {
|
||||||
focusTest(Cascader)
|
focusTest(Cascader)
|
||||||
|
|
||||||
|
@ -187,4 +194,60 @@ describe('Cascader', () => {
|
||||||
expect(wrapper.vm.inputValue).toBe('123')
|
expect(wrapper.vm.inputValue).toBe('123')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('limit filtered item count', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.outerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.outerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
it('limit with positive number', async () => {
|
||||||
|
const wrapper = mount(Cascader, {
|
||||||
|
propsData: { options, showSearch: { filter, limit: 1 }},
|
||||||
|
sync: false,
|
||||||
|
attachToDocument: true,
|
||||||
|
})
|
||||||
|
wrapper.find('input').trigger('click')
|
||||||
|
wrapper.find('input').element.value = 'a'
|
||||||
|
wrapper.find('input').trigger('input')
|
||||||
|
await asyncExpect(() => {
|
||||||
|
expect($$('.ant-cascader-menu-item').length).toBe(1)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('not limit', async () => {
|
||||||
|
const wrapper = mount(Cascader, {
|
||||||
|
propsData: { options, showSearch: { filter, limit: false }},
|
||||||
|
sync: false,
|
||||||
|
attachToDocument: true,
|
||||||
|
})
|
||||||
|
wrapper.find('input').trigger('click')
|
||||||
|
wrapper.find('input').element.value = 'a'
|
||||||
|
wrapper.find('input').trigger('input')
|
||||||
|
await asyncExpect(() => {
|
||||||
|
expect($$('.ant-cascader-menu-item').length).toBe(2)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('negative limit', async () => {
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
const wrapper = mount(Cascader, {
|
||||||
|
propsData: { options, showSearch: { filter, limit: -1 }},
|
||||||
|
sync: false,
|
||||||
|
attachToDocument: true,
|
||||||
|
})
|
||||||
|
wrapper.find('input').trigger('click')
|
||||||
|
wrapper.find('input').element.value = 'a'
|
||||||
|
wrapper.find('input').trigger('input')
|
||||||
|
await asyncExpect(() => {
|
||||||
|
expect($$('.ant-cascader-menu-item').length).toBe(2)
|
||||||
|
}, 0)
|
||||||
|
expect(errorSpy).toBeCalledWith(
|
||||||
|
"Warning: 'limit' of showSearch in Cascader should be positive number or false.",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,6 +34,7 @@ Fields in `showSearch`:
|
||||||
| Property | Description | Type | Default |
|
| Property | Description | Type | Default |
|
||||||
| -------- | ----------- | ---- | ------- |
|
| -------- | ----------- | ---- | ------- |
|
||||||
| filter | The function will receive two arguments, inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded. | `function(inputValue, path): boolean` | |
|
| filter | The function will receive two arguments, inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded. | `function(inputValue, path): boolean` | |
|
||||||
|
| limit | Set the count of filtered items | number \| false | 50 |
|
||||||
| matchInputWidth | Whether the width of result list equals to input's | boolean | |
|
| matchInputWidth | Whether the width of result list equals to input's | boolean | |
|
||||||
| render | Used to render filtered options, you can use slot="showSearchRender" and slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
|
| render | Used to render filtered options, you can use slot="showSearchRender" and slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
|
||||||
| sort | Used to sort filtered options. | `function(a, b, inputValue)` | |
|
| sort | Used to sort filtered options. | `function(a, b, inputValue)` | |
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Icon from '../icon'
|
||||||
import { hasProp, filterEmpty, getOptionProps, getStyle, getClass, getAttrs, getComponentFromProp, isValidElement } from '../_util/props-util'
|
import { hasProp, filterEmpty, getOptionProps, getStyle, getClass, getAttrs, getComponentFromProp, isValidElement } from '../_util/props-util'
|
||||||
import BaseMixin from '../_util/BaseMixin'
|
import BaseMixin from '../_util/BaseMixin'
|
||||||
import { cloneElement } from '../_util/vnode'
|
import { cloneElement } from '../_util/vnode'
|
||||||
|
import warning from '../_util/warning'
|
||||||
|
|
||||||
const CascaderOptionType = PropTypes.shape({
|
const CascaderOptionType = PropTypes.shape({
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
@ -32,6 +33,7 @@ const ShowSearchType = PropTypes.shape({
|
||||||
render: PropTypes.func,
|
render: PropTypes.func,
|
||||||
sort: PropTypes.func,
|
sort: PropTypes.func,
|
||||||
matchInputWidth: PropTypes.bool,
|
matchInputWidth: PropTypes.bool,
|
||||||
|
limit: PropTypes.oneOfType([Boolean, Number]),
|
||||||
}).loose
|
}).loose
|
||||||
function noop () {}
|
function noop () {}
|
||||||
|
|
||||||
|
@ -78,6 +80,9 @@ const CascaderProps = {
|
||||||
suffixIcon: PropTypes.any,
|
suffixIcon: PropTypes.any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We limit the filtered item count by default
|
||||||
|
const defaultLimit = 50
|
||||||
|
|
||||||
function defaultFilterOption (inputValue, path, names) {
|
function defaultFilterOption (inputValue, path, names) {
|
||||||
return path.some(option => option[names.label].indexOf(inputValue) > -1)
|
return path.some(option => option[names.label].indexOf(inputValue) > -1)
|
||||||
}
|
}
|
||||||
|
@ -99,6 +104,26 @@ function getFilledFieldNames ({ fieldNames = {}}) {
|
||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flattenTree (
|
||||||
|
options = [],
|
||||||
|
props,
|
||||||
|
ancestor = [],
|
||||||
|
) {
|
||||||
|
const names = getFilledFieldNames(props)
|
||||||
|
let flattenOptions = []
|
||||||
|
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 = ({ labels }) => labels.join(' / ')
|
const defaultDisplayRender = ({ labels }) => labels.join(' / ')
|
||||||
|
|
||||||
const Cascader = {
|
const Cascader = {
|
||||||
|
@ -110,9 +135,13 @@ const Cascader = {
|
||||||
prop: 'value',
|
prop: 'value',
|
||||||
event: 'change',
|
event: 'change',
|
||||||
},
|
},
|
||||||
|
inject: {
|
||||||
|
configProvider: { default: {}},
|
||||||
|
localeData: { default: {}},
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
this.cachedOptions = []
|
this.cachedOptions = []
|
||||||
const { value, defaultValue, popupVisible, showSearch, options, flattenTree } = this
|
const { value, defaultValue, popupVisible, showSearch, options } = this
|
||||||
return {
|
return {
|
||||||
sValue: value || defaultValue || [],
|
sValue: value || defaultValue || [],
|
||||||
inputValue: '',
|
inputValue: '',
|
||||||
|
@ -137,7 +166,7 @@ const Cascader = {
|
||||||
},
|
},
|
||||||
options (val) {
|
options (val) {
|
||||||
if (this.showSearch) {
|
if (this.showSearch) {
|
||||||
this.setState({ flattenOptions: this.flattenTree(this.options, this.$props) })
|
this.setState({ flattenOptions: flattenTree(val, this.$props) })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -171,11 +200,11 @@ const Cascader = {
|
||||||
|
|
||||||
handlePopupVisibleChange (popupVisible) {
|
handlePopupVisibleChange (popupVisible) {
|
||||||
if (!hasProp(this, 'popupVisible')) {
|
if (!hasProp(this, 'popupVisible')) {
|
||||||
this.setState({
|
this.setState(state => ({
|
||||||
sPopupVisible: popupVisible,
|
sPopupVisible: popupVisible,
|
||||||
inputFocused: popupVisible,
|
inputFocused: popupVisible,
|
||||||
inputValue: popupVisible ? this.inputValue : '',
|
inputValue: popupVisible ? state.inputValue : '',
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
this.$emit('popupVisibleChange', popupVisible)
|
this.$emit('popupVisibleChange', popupVisible)
|
||||||
},
|
},
|
||||||
|
@ -244,24 +273,6 @@ const Cascader = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
flattenTree (options, props, ancestor = []) {
|
|
||||||
const names = getFilledFieldNames(props)
|
|
||||||
let flattenOptions = []
|
|
||||||
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
|
|
||||||
},
|
|
||||||
|
|
||||||
generateFilteredOptions (prefixCls) {
|
generateFilteredOptions (prefixCls) {
|
||||||
const { showSearch, notFoundContent, $scopedSlots } = this
|
const { showSearch, notFoundContent, $scopedSlots } = this
|
||||||
const names = getFilledFieldNames(this.$props)
|
const names = getFilledFieldNames(this.$props)
|
||||||
|
@ -269,11 +280,35 @@ const Cascader = {
|
||||||
filter = defaultFilterOption,
|
filter = defaultFilterOption,
|
||||||
// render = this.defaultRenderFilteredOption,
|
// render = this.defaultRenderFilteredOption,
|
||||||
sort = defaultSortFilteredOption,
|
sort = defaultSortFilteredOption,
|
||||||
|
limit = defaultLimit,
|
||||||
} = showSearch
|
} = showSearch
|
||||||
const { flattenOptions = [], inputValue } = this.$data
|
|
||||||
const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption
|
const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption
|
||||||
const filtered = flattenOptions.filter((path) => filter(inputValue, path, names))
|
const { flattenOptions = [], inputValue } = this.$data
|
||||||
.sort((a, b) => sort(a, b, inputValue, names))
|
|
||||||
|
// Limit the filter if needed
|
||||||
|
let filtered
|
||||||
|
if (limit > 0) {
|
||||||
|
filtered = []
|
||||||
|
let matchCount = 0
|
||||||
|
|
||||||
|
// Perf optimization to filter items only below the limit
|
||||||
|
flattenOptions.some(path => {
|
||||||
|
const match = filter(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(inputValue, path, names))
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.sort((a, b) => sort(a, b, inputValue, names))
|
||||||
|
|
||||||
if (filtered.length > 0) {
|
if (filtered.length > 0) {
|
||||||
return filtered.map((path) => {
|
return filtered.map((path) => {
|
||||||
|
@ -307,14 +342,22 @@ const Cascader = {
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { $slots, sPopupVisible, inputValue, $listeners } = this
|
const { $slots, sPopupVisible, inputValue, $listeners, configProvider, localeData } = this
|
||||||
const { sValue: value, inputFocused } = this.$data
|
const { sValue: value, inputFocused } = this.$data
|
||||||
const props = getOptionProps(this)
|
const props = getOptionProps(this)
|
||||||
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
|
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
|
||||||
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
|
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
|
||||||
|
const { getPopupContainer: getContextPopupContainer } = configProvider
|
||||||
const {
|
const {
|
||||||
prefixCls, inputPrefixCls, placeholder, size, disabled,
|
prefixCls,
|
||||||
allowClear, showSearch = false, ...otherProps } = props
|
inputPrefixCls,
|
||||||
|
placeholder = localeData.placeholder,
|
||||||
|
size,
|
||||||
|
disabled,
|
||||||
|
allowClear,
|
||||||
|
showSearch = false,
|
||||||
|
...otherProps
|
||||||
|
} = props
|
||||||
|
|
||||||
const sizeCls = classNames({
|
const sizeCls = classNames({
|
||||||
[`${inputPrefixCls}-lg`]: size === 'large',
|
[`${inputPrefixCls}-lg`]: size === 'large',
|
||||||
|
@ -448,9 +491,11 @@ const Cascader = {
|
||||||
<Icon type='redo' spin />
|
<Icon type='redo' spin />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
const getPopupContainer = props.getPopupContainer || getContextPopupContainer
|
||||||
const cascaderProps = {
|
const cascaderProps = {
|
||||||
props: {
|
props: {
|
||||||
...props,
|
...props,
|
||||||
|
getPopupContainer,
|
||||||
options: options,
|
options: options,
|
||||||
value: value,
|
value: value,
|
||||||
popupVisible: sPopupVisible,
|
popupVisible: sPopupVisible,
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
| 参数 | 说明 | 类型 | 默认值 |
|
| 参数 | 说明 | 类型 | 默认值 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| filter | 接收 `inputValue` `path` 两个参数,当 `path` 符合筛选条件时,应返回 true,反之则返回 false。 | `function(inputValue, path): boolean` | |
|
| filter | 接收 `inputValue` `path` 两个参数,当 `path` 符合筛选条件时,应返回 true,反之则返回 false。 | `function(inputValue, path): boolean` | |
|
||||||
|
| limit | 搜索结果展示数量 | number \| false | 50 |
|
||||||
| matchInputWidth | 搜索结果列表是否与输入框同宽 | boolean | |
|
| matchInputWidth | 搜索结果列表是否与输入框同宽 | boolean | |
|
||||||
| render | 用于渲染 filter 后的选项,可使用slot="showSearchRender" 和 slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
|
| render | 用于渲染 filter 后的选项,可使用slot="showSearchRender" 和 slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
|
||||||
| sort | 用于排序 filter 后的选项 | `function(a, b, inputValue)` | |
|
| sort | 用于排序 filter 后的选项 | `function(a, b, inputValue)` | |
|
||||||
|
|
Loading…
Reference in New Issue