tjz
7 years ago
9 changed files with 683 additions and 6 deletions
@ -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