parent
de23e4de11
commit
6e8b5b9a20
@ -0,0 +1,53 @@
|
||||
|
||||
<cn>
|
||||
#### 基本
|
||||
省市区级联。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Basic
|
||||
Cascade selection box for selecting province/city/district.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-cascader :options="options" @change="onChange" placeholder="Please select" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
options: [{
|
||||
value: 'zhejiang',
|
||||
label: 'Zhejiang',
|
||||
children: [{
|
||||
value: 'hangzhou',
|
||||
label: 'Hangzhou',
|
||||
children: [{
|
||||
value: 'xihu',
|
||||
label: 'West Lake',
|
||||
}],
|
||||
}],
|
||||
}, {
|
||||
value: 'jiangsu',
|
||||
label: 'Jiangsu',
|
||||
children: [{
|
||||
value: 'nanjing',
|
||||
label: 'Nanjing',
|
||||
children: [{
|
||||
value: 'zhonghuamen',
|
||||
label: 'Zhong Hua Men',
|
||||
}],
|
||||
}],
|
||||
}]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(value) {
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
@ -0,0 +1,374 @@
|
||||
<script>
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import VcCascader from '../vc-cascader'
|
||||
import arrayTreeFilter from 'array-tree-filter'
|
||||
import classNames from 'classnames'
|
||||
import omit from 'omit.js'
|
||||
import KeyCode from '../_util/KeyCode'
|
||||
import Input from '../input'
|
||||
import Icon from '../icon'
|
||||
import { hasProp, filterEmpty, getOptionProps } from '../_util/props-util'
|
||||
import BaseMixin from '../_util/BaseMixin'
|
||||
|
||||
const CascaderOptionType = PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.any.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.array,
|
||||
__IS_FILTERED_OPTION: PropTypes.bool,
|
||||
}).loose
|
||||
|
||||
const CascaderExpandTrigger = PropTypes.oneOf(['click', 'hover'])
|
||||
|
||||
const ShowSearchType = PropTypes.shape({
|
||||
filter: PropTypes.func,
|
||||
render: PropTypes.func,
|
||||
sort: PropTypes.func,
|
||||
matchInputWidth: PropTypes.bool,
|
||||
}).loose
|
||||
function noop () {}
|
||||
|
||||
const CascaderProps = {
|
||||
/** 可选项数据源 */
|
||||
options: PropTypes.arrayOf(CascaderOptionType).def([]),
|
||||
/** 默认的选中项 */
|
||||
defaultValue: PropTypes.arrayOf(PropTypes.string),
|
||||
/** 指定选中项 */
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
/** 选择完成后的回调 */
|
||||
// onChange?: (value: string[], selectedOptions?: CascaderOptionType[]) => void;
|
||||
/** 选择后展示的渲染函数 */
|
||||
displayRender: PropTypes.func,
|
||||
transitionName: PropTypes.string.def('slide-up'),
|
||||
popupStyle: PropTypes.object.def({}),
|
||||
/** 自定义浮层类名 */
|
||||
popupClassName: PropTypes.string,
|
||||
/** 浮层预设位置:`bottomLeft` `bottomRight` `topLeft` `topRight` */
|
||||
popupPlacement: PropTypes.oneOf(['bottomLeft', 'bottomRight', 'topLeft', 'topRight']).def('bottomLeft'),
|
||||
/** 输入框占位文本*/
|
||||
placeholder: PropTypes.string.def('Please select'),
|
||||
/** 输入框大小,可选 `large` `default` `small` */
|
||||
size: PropTypes.oneOf(['large', 'default', 'small']),
|
||||
/** 禁用*/
|
||||
disabled: PropTypes.bool.def(false),
|
||||
/** 是否支持清除*/
|
||||
allowClear: PropTypes.bool.def(true),
|
||||
showSearch: PropTypes.oneOfType([PropTypes.bool, ShowSearchType]),
|
||||
notFoundContent: PropTypes.any.def('Not Found'),
|
||||
loadData: PropTypes.func,
|
||||
/** 次级菜单的展开方式,可选 'click' 和 'hover' */
|
||||
expandTrigger: CascaderExpandTrigger,
|
||||
/** 当此项为 true 时,点选每级菜单选项值都会发生变化 */
|
||||
changeOnSelect: PropTypes.bool,
|
||||
/** 浮层可见变化时回调 */
|
||||
// onPopupVisibleChange?: (popupVisible: boolean) => void;
|
||||
prefixCls: PropTypes.string.def('ant-cascader'),
|
||||
inputPrefixCls: PropTypes.string.def('ant-input'),
|
||||
getPopupContainer: PropTypes.func,
|
||||
popupVisible: PropTypes.bool,
|
||||
}
|
||||
|
||||
function defaultFilterOption (inputValue, path) {
|
||||
return path.some(option => option.label.indexOf(inputValue) > -1)
|
||||
}
|
||||
|
||||
function defaultSortFilteredOption (a, b, inputValue) {
|
||||
function callback (elem) {
|
||||
return elem.label.indexOf(inputValue) > -1
|
||||
}
|
||||
|
||||
return a.findIndex(callback) - b.findIndex(callback)
|
||||
}
|
||||
|
||||
const defaultDisplayRender = (label) => label.join(' / ')
|
||||
|
||||
export default {
|
||||
mixins: [BaseMixin],
|
||||
props: CascaderProps,
|
||||
data () {
|
||||
this.cachedOptions = []
|
||||
const { value, defaultValue, popupVisible, showSearch, options, changeOnSelect, flattenTree } = this
|
||||
return {
|
||||
sValue: value || defaultValue || [],
|
||||
inputValue: '',
|
||||
inputFocused: false,
|
||||
sPopupVisible: popupVisible,
|
||||
flattenOptions: showSearch && flattenTree(options, changeOnSelect),
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.setState({ sValue: val || [] })
|
||||
},
|
||||
popupVisible (val) {
|
||||
this.setState({ sPopupVisible: val })
|
||||
},
|
||||
options (val) {
|
||||
if (this.showSearch) {
|
||||
this.setState({ flattenOptions: this.flattenTree(this.options, this.changeOnSelect) })
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
highlightKeyword (str, keyword, prefixCls) {
|
||||
return str.split(keyword)
|
||||
.map((node, index) => index === 0 ? node : [
|
||||
<span class={`${prefixCls}-menu-item-keyword`} key='seperator'>{keyword}</span>,
|
||||
node,
|
||||
])
|
||||
},
|
||||
|
||||
defaultRenderFilteredOption (inputValue, path, prefixCls) {
|
||||
return path.map(({ label }, index) => {
|
||||
const node = label.indexOf(inputValue) > -1
|
||||
? this.highlightKeyword(label, inputValue, prefixCls) : label
|
||||
return index === 0 ? node : [' / ', node]
|
||||
})
|
||||
},
|
||||
handleChange (value, selectedOptions) {
|
||||
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)
|
||||
},
|
||||
|
||||
handlePopupVisibleChange (popupVisible) {
|
||||
if (!hasProp(this, 'popupVisible')) {
|
||||
this.setState({
|
||||
sPopupVisible: popupVisible,
|
||||
inputFocused: popupVisible,
|
||||
inputValue: popupVisible ? this.inputValue : '',
|
||||
})
|
||||
}
|
||||
this.$emit('popupVisibleChange', popupVisible)
|
||||
},
|
||||
|
||||
handleInputBlur () {
|
||||
this.setState({
|
||||
inputFocused: false,
|
||||
})
|
||||
},
|
||||
|
||||
handleInputClick (e) {
|
||||
const { inputFocused, sPopupVisible } = this
|
||||
// Prevent `Trigger` behaviour.
|
||||
if (inputFocused || sPopupVisible) {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === KeyCode.BACKSPACE) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
|
||||
handleInputChange (e) {
|
||||
const inputValue = e.target.value
|
||||
this.setState({ inputValue })
|
||||
},
|
||||
|
||||
setValue (value, selectedOptions) {
|
||||
if (!hasProp(this, 'value')) {
|
||||
this.setState({ sValue: value })
|
||||
}
|
||||
this.$emit('change', value, selectedOptions)
|
||||
},
|
||||
|
||||
getLabel () {
|
||||
const { options, displayRender = defaultDisplayRender } = this
|
||||
const value = this.sValue
|
||||
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)
|
||||
},
|
||||
|
||||
clearSelection (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!this.inputValue) {
|
||||
this.setValue([])
|
||||
this.handlePopupVisibleChange(false)
|
||||
} else {
|
||||
this.setState({ inputValue: '' })
|
||||
}
|
||||
},
|
||||
|
||||
flattenTree (options, changeOnSelect, ancestor = []) {
|
||||
let flattenOptions = []
|
||||
options.forEach((option) => {
|
||||
const path = ancestor.concat(option)
|
||||
if (changeOnSelect || !option.children || !option.children.length) {
|
||||
flattenOptions.push(path)
|
||||
}
|
||||
if (option.children) {
|
||||
flattenOptions = flattenOptions.concat(this.flattenTree(option.children, changeOnSelect, path))
|
||||
}
|
||||
})
|
||||
return flattenOptions
|
||||
},
|
||||
|
||||
generateFilteredOptions (prefixCls) {
|
||||
const { showSearch, notFoundContent, flattenOptions, inputValue } = this
|
||||
const {
|
||||
filter = defaultFilterOption,
|
||||
render = this.defaultRenderFilteredOption,
|
||||
sort = defaultSortFilteredOption,
|
||||
} = showSearch
|
||||
const filtered = flattenOptions.filter((path) => filter(inputValue, path))
|
||||
.sort((a, b) => sort(a, b, inputValue))
|
||||
|
||||
if (filtered.length > 0) {
|
||||
return filtered.map((path) => {
|
||||
return {
|
||||
__IS_FILTERED_OPTION: true,
|
||||
path,
|
||||
label: render(inputValue, path, prefixCls),
|
||||
value: path.map((o) => o.value),
|
||||
disabled: path.some((o) => o.disabled),
|
||||
}
|
||||
})
|
||||
}
|
||||
return [{ label: notFoundContent, value: 'ANT_CASCADER_NOT_FOUND', disabled: true }]
|
||||
},
|
||||
|
||||
focus () {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
|
||||
blur () {
|
||||
this.$refs.input.blur()
|
||||
},
|
||||
},
|
||||
|
||||
render () {
|
||||
const { $slots, sValue: value, sPopupVisible, inputValue } = this
|
||||
const props = getOptionProps(this)
|
||||
const {
|
||||
prefixCls, inputPrefixCls, placeholder, size, disabled,
|
||||
allowClear, showSearch = false, ...otherProps } = props
|
||||
|
||||
const sizeCls = classNames({
|
||||
[`${inputPrefixCls}-lg`]: size === 'large',
|
||||
[`${inputPrefixCls}-sm`]: size === 'small',
|
||||
})
|
||||
const clearIcon = (allowClear && !disabled && value.length > 0) || inputValue ? (
|
||||
<Icon
|
||||
type='cross-circle'
|
||||
class={`${prefixCls}-picker-clear`}
|
||||
onClick={this.clearSelection}
|
||||
/>
|
||||
) : null
|
||||
const arrowCls = classNames({
|
||||
[`${prefixCls}-picker-arrow`]: true,
|
||||
[`${prefixCls}-picker-arrow-expand`]: sPopupVisible,
|
||||
})
|
||||
const pickerCls = classNames(
|
||||
`${prefixCls}-picker`, {
|
||||
[`${prefixCls}-picker-with-value`]: inputValue,
|
||||
[`${prefixCls}-picker-disabled`]: disabled,
|
||||
[`${prefixCls}-picker-${size}`]: !!size,
|
||||
})
|
||||
|
||||
// Fix bug of https://github.com/facebook/react/pull/5004
|
||||
// and https://fb.me/react-unknown-prop
|
||||
const tempInputProps = omit(otherProps, [
|
||||
'options',
|
||||
'popupPlacement',
|
||||
'transitionName',
|
||||
'displayRender',
|
||||
'changeOnSelect',
|
||||
'expandTrigger',
|
||||
'popupVisible',
|
||||
'getPopupContainer',
|
||||
'loadData',
|
||||
'popupClassName',
|
||||
'filterOption',
|
||||
'renderFilteredOption',
|
||||
'sortFilteredOption',
|
||||
'notFoundContent',
|
||||
])
|
||||
|
||||
let options = this.options
|
||||
if (inputValue) {
|
||||
options = this.generateFilteredOptions(prefixCls)
|
||||
}
|
||||
// Dropdown menu should keep previous status until it is fully closed.
|
||||
if (!sPopupVisible) {
|
||||
options = this.cachedOptions
|
||||
} else {
|
||||
this.cachedOptions = options
|
||||
}
|
||||
|
||||
const dropdownMenuColumnStyle = {}
|
||||
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
|
||||
if (resultListMatchInputWidth && inputValue && this.input) {
|
||||
dropdownMenuColumnStyle.width = this.input.input.offsetWidth
|
||||
}
|
||||
const inputProps = {
|
||||
props: {
|
||||
...tempInputProps,
|
||||
prefixCls: inputPrefixCls,
|
||||
placeholder: value && value.length > 0 ? undefined : placeholder,
|
||||
value: inputValue,
|
||||
disabled: disabled,
|
||||
readOnly: !showSearch,
|
||||
autoComplete: 'off',
|
||||
},
|
||||
class: `${prefixCls}-input ${sizeCls}`,
|
||||
ref: 'input',
|
||||
on: {
|
||||
click: showSearch ? this.handleInputClick : noop,
|
||||
blur: showSearch ? this.handleInputBlur : noop,
|
||||
keydown: this.handleKeyDown,
|
||||
change: showSearch ? this.handleInputChange : noop,
|
||||
},
|
||||
}
|
||||
const children = filterEmpty($slots.default)
|
||||
const input = children.length ? children : (
|
||||
<span
|
||||
class={pickerCls}
|
||||
>
|
||||
<span class={`${prefixCls}-picker-label`}>
|
||||
{this.getLabel()}
|
||||
</span>
|
||||
<Input {...inputProps}/>
|
||||
{clearIcon}
|
||||
<Icon type='down' class={arrowCls} />
|
||||
</span>
|
||||
)
|
||||
const cascaderProps = {
|
||||
props: {
|
||||
...props,
|
||||
options: options,
|
||||
value: value,
|
||||
popupVisible: sPopupVisible,
|
||||
dropdownMenuColumnStyle: dropdownMenuColumnStyle,
|
||||
},
|
||||
on: {
|
||||
popupVisibleChange: this.handlePopupVisibleChange,
|
||||
change: this.handleChange,
|
||||
},
|
||||
}
|
||||
return (
|
||||
<VcCascader {...cascaderProps}>
|
||||
{input}
|
||||
</VcCascader>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
@ -0,0 +1,5 @@
|
||||
import '../../style/index.less'
|
||||
import './index.less'
|
||||
|
||||
// style dependencies
|
||||
import '../../input/style'
|
@ -0,0 +1,214 @@
|
||||
@import "../../style/themes/default";
|
||||
@import "../../style/mixins/index";
|
||||
@import "../../input/style/mixin";
|
||||
|
||||
@cascader-prefix-cls: ~"@{ant-prefix}-cascader";
|
||||
|
||||
.@{cascader-prefix-cls} {
|
||||
.reset-component;
|
||||
|
||||
&-input.@{ant-prefix}-input {
|
||||
// Add important to fix https://github.com/ant-design/ant-design/issues/5078
|
||||
// because input.less will compile after cascader.less
|
||||
background-color: transparent !important;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-picker {
|
||||
.reset-component;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-color: @component-background;
|
||||
border-radius: @border-radius-base;
|
||||
outline: 0;
|
||||
|
||||
&-with-value &-label {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
background: @input-disabled-bg;
|
||||
color: @disabled-color;
|
||||
.@{cascader-prefix-cls}-input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus .@{cascader-prefix-cls}-input {
|
||||
.active;
|
||||
}
|
||||
|
||||
&-label {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
top: 50%;
|
||||
margin-top: -10px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: 0 @control-padding-horizontal;
|
||||
}
|
||||
|
||||
&-clear {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: @control-padding-horizontal;
|
||||
z-index: 2;
|
||||
background: @component-background;
|
||||
top: 50%;
|
||||
font-size: @font-size-sm;
|
||||
color: @disabled-color;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: -6px;
|
||||
line-height: 12px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease, opacity 0.15s ease;
|
||||
&:hover {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-clear {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// arrow
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
right: @control-padding-horizontal;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
margin-top: -6px;
|
||||
line-height: 12px;
|
||||
color: @disabled-color;
|
||||
&:before {
|
||||
transition: transform .2s;
|
||||
}
|
||||
&&-expand {
|
||||
&:before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-picker-small &-picker-clear,
|
||||
&-picker-small &-picker-arrow {
|
||||
right: @control-padding-horizontal-sm;
|
||||
}
|
||||
|
||||
&-menus {
|
||||
font-size: @font-size-base;
|
||||
background: @component-background;
|
||||
position: absolute;
|
||||
z-index: @zindex-dropdown;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: @box-shadow-base;
|
||||
white-space: nowrap;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-empty,
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
|
||||
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
|
||||
animation-name: antSlideUpIn;
|
||||
}
|
||||
|
||||
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
|
||||
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
|
||||
animation-name: antSlideDownIn;
|
||||
}
|
||||
|
||||
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
|
||||
animation-name: antSlideUpOut;
|
||||
}
|
||||
|
||||
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
|
||||
animation-name: antSlideDownOut;
|
||||
}
|
||||
}
|
||||
&-menu {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
min-width: 111px;
|
||||
height: 180px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-right: @border-width-base @border-style-base @border-color-split;
|
||||
overflow: auto;
|
||||
&:first-child {
|
||||
border-radius: @border-radius-base 0 0 @border-radius-base;
|
||||
}
|
||||
&:last-child {
|
||||
border-right-color: transparent;
|
||||
margin-right: -1px;
|
||||
border-radius: 0 @border-radius-base @border-radius-base 0;
|
||||
}
|
||||
&:only-child {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
&-menu-item {
|
||||
padding: 5px @control-padding-horizontal;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background: @item-hover-bg;
|
||||
}
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
color: @disabled-color;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&-active:not(&-disabled) {
|
||||
&,
|
||||
&:hover {
|
||||
background: @background-color-base;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
&-expand {
|
||||
position: relative;
|
||||
padding-right: 24px;
|
||||
&:after {
|
||||
.iconfont-font("\e61f");
|
||||
.iconfont-size-under-12px(8px);
|
||||
color: @text-color-secondary;
|
||||
position: absolute;
|
||||
right: @control-padding-horizontal;
|
||||
}
|
||||
}
|
||||
&-loading:after {
|
||||
.iconfont-font("\e64d");
|
||||
animation: loadingCircle 1s infinite linear;
|
||||
}
|
||||
|
||||
& &-keyword {
|
||||
color: @highlight-color;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue