mirror of
https://gitee.com/ant-design-vue/ant-design-vue.git
synced 2024-12-04 04:58:16 +08:00
feat: cascader support limit
This commit is contained in:
parent
843b074fed
commit
3a49503baa
@ -4,6 +4,9 @@ import KeyCode from '../../_util/KeyCode'
|
||||
import Cascader from '..'
|
||||
import focusTest from '../../../tests/shared/focusTest'
|
||||
|
||||
function $$ (className) {
|
||||
return document.body.querySelectorAll(className)
|
||||
}
|
||||
const options = [{
|
||||
value: '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', () => {
|
||||
focusTest(Cascader)
|
||||
|
||||
@ -187,4 +194,60 @@ describe('Cascader', () => {
|
||||
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 |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| 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 | |
|
||||
| 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)` | |
|
||||
|
@ -10,6 +10,7 @@ import Icon from '../icon'
|
||||
import { hasProp, filterEmpty, getOptionProps, getStyle, getClass, getAttrs, getComponentFromProp, isValidElement } from '../_util/props-util'
|
||||
import BaseMixin from '../_util/BaseMixin'
|
||||
import { cloneElement } from '../_util/vnode'
|
||||
import warning from '../_util/warning'
|
||||
|
||||
const CascaderOptionType = PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
@ -32,6 +33,7 @@ const ShowSearchType = PropTypes.shape({
|
||||
render: PropTypes.func,
|
||||
sort: PropTypes.func,
|
||||
matchInputWidth: PropTypes.bool,
|
||||
limit: PropTypes.oneOfType([Boolean, Number]),
|
||||
}).loose
|
||||
function noop () {}
|
||||
|
||||
@ -78,6 +80,9 @@ const CascaderProps = {
|
||||
suffixIcon: PropTypes.any,
|
||||
}
|
||||
|
||||
// We limit the filtered item count by default
|
||||
const defaultLimit = 50
|
||||
|
||||
function defaultFilterOption (inputValue, path, names) {
|
||||
return path.some(option => option[names.label].indexOf(inputValue) > -1)
|
||||
}
|
||||
@ -99,6 +104,26 @@ function getFilledFieldNames ({ fieldNames = {}}) {
|
||||
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 Cascader = {
|
||||
@ -110,9 +135,13 @@ const Cascader = {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
inject: {
|
||||
configProvider: { default: {}},
|
||||
localeData: { default: {}},
|
||||
},
|
||||
data () {
|
||||
this.cachedOptions = []
|
||||
const { value, defaultValue, popupVisible, showSearch, options, flattenTree } = this
|
||||
const { value, defaultValue, popupVisible, showSearch, options } = this
|
||||
return {
|
||||
sValue: value || defaultValue || [],
|
||||
inputValue: '',
|
||||
@ -137,7 +166,7 @@ const Cascader = {
|
||||
},
|
||||
options (val) {
|
||||
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) {
|
||||
if (!hasProp(this, 'popupVisible')) {
|
||||
this.setState({
|
||||
this.setState(state => ({
|
||||
sPopupVisible: popupVisible,
|
||||
inputFocused: popupVisible,
|
||||
inputValue: popupVisible ? this.inputValue : '',
|
||||
})
|
||||
inputValue: popupVisible ? state.inputValue : '',
|
||||
}))
|
||||
}
|
||||
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) {
|
||||
const { showSearch, notFoundContent, $scopedSlots } = this
|
||||
const names = getFilledFieldNames(this.$props)
|
||||
@ -269,11 +280,35 @@ const Cascader = {
|
||||
filter = defaultFilterOption,
|
||||
// render = this.defaultRenderFilteredOption,
|
||||
sort = defaultSortFilteredOption,
|
||||
limit = defaultLimit,
|
||||
} = showSearch
|
||||
const { flattenOptions = [], inputValue } = this.$data
|
||||
const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption
|
||||
const filtered = flattenOptions.filter((path) => filter(inputValue, path, names))
|
||||
.sort((a, b) => sort(a, b, inputValue, names))
|
||||
const { flattenOptions = [], inputValue } = this.$data
|
||||
|
||||
// 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) {
|
||||
return filtered.map((path) => {
|
||||
@ -307,14 +342,22 @@ const Cascader = {
|
||||
},
|
||||
|
||||
render () {
|
||||
const { $slots, sPopupVisible, inputValue, $listeners } = this
|
||||
const { $slots, sPopupVisible, inputValue, $listeners, configProvider, localeData } = this
|
||||
const { sValue: value, inputFocused } = this.$data
|
||||
const props = getOptionProps(this)
|
||||
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
|
||||
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
|
||||
const { getPopupContainer: getContextPopupContainer } = configProvider
|
||||
const {
|
||||
prefixCls, inputPrefixCls, placeholder, size, disabled,
|
||||
allowClear, showSearch = false, ...otherProps } = props
|
||||
prefixCls,
|
||||
inputPrefixCls,
|
||||
placeholder = localeData.placeholder,
|
||||
size,
|
||||
disabled,
|
||||
allowClear,
|
||||
showSearch = false,
|
||||
...otherProps
|
||||
} = props
|
||||
|
||||
const sizeCls = classNames({
|
||||
[`${inputPrefixCls}-lg`]: size === 'large',
|
||||
@ -448,9 +491,11 @@ const Cascader = {
|
||||
<Icon type='redo' spin />
|
||||
</span>
|
||||
)
|
||||
const getPopupContainer = props.getPopupContainer || getContextPopupContainer
|
||||
const cascaderProps = {
|
||||
props: {
|
||||
...props,
|
||||
getPopupContainer,
|
||||
options: options,
|
||||
value: value,
|
||||
popupVisible: sPopupVisible,
|
||||
|
@ -34,6 +34,7 @@
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| filter | 接收 `inputValue` `path` 两个参数,当 `path` 符合筛选条件时,应返回 true,反之则返回 false。 | `function(inputValue, path): boolean` | |
|
||||
| limit | 搜索结果展示数量 | number \| false | 50 |
|
||||
| matchInputWidth | 搜索结果列表是否与输入框同宽 | boolean | |
|
||||
| render | 用于渲染 filter 后的选项,可使用slot="showSearchRender" 和 slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
|
||||
| sort | 用于排序 filter 后的选项 | `function(a, b, inputValue)` | |
|
||||
|
Loading…
Reference in New Issue
Block a user