385 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			385 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
| 
 | |
| 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 = ({ labels }) => labels.join(' / ')
 | |
| 
 | |
| export default {
 | |
|   mixins: [BaseMixin],
 | |
|   props: CascaderProps,
 | |
|   model: {
 | |
|     prop: 'value',
 | |
|     event: 'change',
 | |
|   },
 | |
|   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, $scopedSlots } = this
 | |
|       const displayRender = this.displayRender || $scopedSlots.displayRender || defaultDisplayRender
 | |
|       const value = this.sValue
 | |
|       const unwrappedValue = Array.isArray(value[0]) ? value[0] : value
 | |
|       const selectedOptions = arrayTreeFilter(options,
 | |
|         (o, level) => o.value === unwrappedValue[level],
 | |
|       )
 | |
|       const labels = selectedOptions.map(o => o.label)
 | |
|       return displayRender({ labels, 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, $scopedSlots } = this
 | |
|       const {
 | |
|         filter = defaultFilterOption,
 | |
|         // render = this.defaultRenderFilteredOption,
 | |
|         sort = defaultSortFilteredOption,
 | |
|       } = showSearch
 | |
|       const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption
 | |
|       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}
 | |
|         key='clear-icon'
 | |
|       />
 | |
|     ) : 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',
 | |
|       'defaultValue',
 | |
|     ])
 | |
| 
 | |
|     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}
 | |
|       >
 | |
|         { showSearch ? <span class={`${prefixCls}-picker-label`}>
 | |
|           {this.getLabel()}
 | |
|         </span> : null}
 | |
|         <Input {...inputProps}/>
 | |
|         { !showSearch ? <span class={`${prefixCls}-picker-label`}>
 | |
|           {this.getLabel()}
 | |
|         </span> : null}
 | |
|         {clearIcon}
 | |
|         <Icon type='down' key='down-icon' 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>
 | |
|     )
 | |
|   },
 | |
| }
 | |
| 
 |