import PropTypes from '../../_util/vue-types' import KeyCode from '../../_util/KeyCode' import classnames from 'classnames' import pick from 'lodash/pick' import omit from 'omit.js' import { getPropValue, getValuePropValue, isMultiple, toArray, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, preventDefaultEvent, getTreeNodesStates, flatToHierarchy, filterParentPosition, isPositionPrefix, labelCompatible, loopAllChildren, filterAllCheckedData, processSimpleTreeData, toTitle, } from './util' import SelectTrigger from './SelectTrigger' import _TreeNode from './TreeNode' import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies' import { SelectPropTypes } from './PropTypes' import { initDefaultProps, getOptionProps, hasProp, getAllProps, getComponentFromProp } from '../../_util/props-util' import BaseMixin from '../../_util/BaseMixin' import getTransitionProps from '../../_util/getTransitionProps' function noop () { } function filterFn (input, child) { return String(getPropValue(child, labelCompatible(this.$props.treeNodeFilterProp))) .indexOf(input) > -1 } const defaultProps = { prefixCls: 'rc-tree-select', // filterTreeNode: filterFn, // [Legacy] TODO: Set false and filter not hide? showSearch: true, allowClear: false, // placeholder: '', // searchPlaceholder: '', labelInValue: false, // onClick: noop, // onChange: noop, // onSelect: noop, // onDeselect: noop, // onSearch: noop, showArrow: true, dropdownMatchSelectWidth: true, dropdownStyle: {}, dropdownVisibleChange: () => { return true }, notFoundContent: 'Not Found', showCheckedStrategy: SHOW_CHILD, // skipHandleInitValue: false, // Deprecated (use treeCheckStrictly) treeCheckStrictly: false, treeIcon: false, treeLine: false, treeDataSimpleMode: false, treeDefaultExpandAll: false, treeCheckable: false, treeNodeFilterProp: 'value', treeNodeLabelProp: 'title', } const Select = { mixins: [BaseMixin], name: 'VCTreeSelect', props: initDefaultProps({ ...SelectPropTypes, __propsSymbol__: PropTypes.any }, defaultProps), data () { let value = [] const props = getOptionProps(this) this.preProps = { ...props } if ('value' in props) { value = toArray(props.value) } else { value = toArray(props.defaultValue) } // save parsed treeData, for performance (treeData may be very big) this.renderedTreeData = this.renderTreeData() value = this.addLabelToValue(props, value) value = this.getValue(props, value, props.inputValue ? '__strict' : true) const inputValue = props.inputValue || '' // if (props.combobox) { // inputValue = value.length ? String(value[0].value) : ''; // } return { sValue: value, sInputValue: inputValue, sOpen: props.open || props.defaultOpen, sFocused: false, } }, mounted () { this.$nextTick(() => { const { autoFocus, disabled } = this if (isMultiple(this.$props)) { const inputNode = this.getInputDOMNode() if (inputNode.value) { inputNode.style.width = '' inputNode.style.width = `${this.$refs.inputMirrorInstance.clientWidth || this.$refs.inputMirrorInstance.offsetWidth}px` } else { inputNode.style.width = '' } } if (autoFocus && !disabled) { this.focus() } }) }, watch: { // for performance (use __propsSymbol__ avoid deep watch) __propsSymbol__ () { const nextProps = getOptionProps(this) // save parsed treeData, for performance (treeData may be very big) this.renderedTreeData = this.renderTreeData(nextProps) // Detecting whether the object of `onChange`'s argument is old ref. // Better to do a deep equal later. this._cacheTreeNodesStates = this._cacheTreeNodesStates !== 'no' && this._savedValue && nextProps.value === this._savedValue if (this.preProps.treeData !== nextProps.treeData || this.preProps.children !== nextProps.children) { // refresh this._treeNodesStates cache this._treeNodesStates = getTreeNodesStates( this.renderedTreeData || nextProps.children, this.sValue.map(item => item.value) ) } if ('value' in nextProps) { let value = toArray(nextProps.value) value = this.addLabelToValue(nextProps, value) value = this.getValue(nextProps, value) this.setState({ sValue: value, }, this.forcePopupAlign) // if (nextProps.combobox) { // this.setState({ // inputValue: value.length ? String(value[0].key) : '', // }); // } } if (nextProps.inputValue !== this.preProps.inputValue) { this.setState({ sInputValue: nextProps.inputValue, }) } if ('open' in nextProps) { this.setState({ sOpen: nextProps.open, }) } this.preProps = { ...nextProps } }, }, beforeUpdate () { if (this._savedValue && this.$props.value && this.$props.value !== this._savedValue && this.$props.value === this.preProps.value) { this._cacheTreeNodesStates = false this.getValue(this.$props, this.addLabelToValue(this.$props, toArray(this.$props.value))) } }, updated () { const state = this.$data const props = this.$props if (state.sOpen && isMultiple(props)) { this.$nextTick(() => { const inputNode = this.getInputDOMNode() if (inputNode.value) { inputNode.style.width = '' inputNode.style.width = `${this.$refs.inputMirrorInstance.clientWidth}px` } else { inputNode.style.width = '' } }) } }, beforeDestroy () { this.clearDelayTimer() if (this.dropdownContainer) { document.body.removeChild(this.dropdownContainer) this.dropdownContainer = null } }, methods: { loopTreeData (data, level = 0, treeCheckable) { return data.map((item, index) => { const pos = `${level}-${index}` const { label, value, disabled, key, selectable, children, isLeaf, ...otherProps } = item const tnProps = { ...pick(item, ['on', 'class', 'style']), props: { value, title: label, disabled: disabled || false, selectable: selectable === false ? selectable : !treeCheckable, ...omit(otherProps, ['on', 'class', 'style']), }, key: key || value || pos, } let ret if (children && children.length) { ret = (<_TreeNode {...tnProps}>{this.loopTreeData(children, pos, treeCheckable)}) } else { ret = (<_TreeNode {...tnProps} isLeaf={isLeaf}/>) } return ret }) }, onInputChange (event) { const val = event.target.value const { $props: props } = this this.setState({ sInputValue: val, sOpen: true, }, this.forcePopupAlign) if (props.treeCheckable && !val) { this.setState({ sValue: this.getValue(props, [...this.sValue], false), }) } this.__emit('search', val) }, onDropdownVisibleChange (open) { // selection inside combobox cause click if (!open && document.activeElement === this.getInputDOMNode()) { // return; } this.setOpenState(open, undefined, !open) }, // combobox ignore onKeyDown (event) { const props = this.$props if (props.disabled) { return } const keyCode = event.keyCode if (this.sOpen && !this.getInputDOMNode()) { this.onInputKeyDown(event) } else if (keyCode === KeyCode.ENTER || keyCode === KeyCode.DOWN) { this.setOpenState(true) event.preventDefault() } }, onInputKeyDown (event) { const props = this.$props if (props.disabled) { return } const state = this.$data const keyCode = event.keyCode if (isMultiple(props) && !event.target.value && keyCode === KeyCode.BACKSPACE) { const value = state.sValue.concat() if (value.length) { const popValue = value.pop() this.removeSelected(this.isLabelInValue() ? popValue : popValue.value) } return } if (keyCode === KeyCode.DOWN) { if (!state.sOpen) { this.openIfHasChildren() event.preventDefault() event.stopPropagation() return } } else if (keyCode === KeyCode.ESC) { if (state.sOpen) { this.setOpenState(false) event.preventDefault() event.stopPropagation() } return } }, onSelect (selectedKeys, info) { const item = info.node let value = this.sValue const props = this.$props const selectedValue = getValuePropValue(item) const selectedLabel = this.getLabelFromNode(item) const checkableSelect = props.treeCheckable && info.event === 'select' let event = selectedValue if (this.isLabelInValue()) { event = { value: event, label: selectedLabel, } } if (info.selected === false) { this.onDeselect(info) if (!checkableSelect) return } this.__emit('select', event, item, info) const checkEvt = info.event === 'check' if (isMultiple(props)) { this.$nextTick(() => { // clearSearchInput will change sInputValue this.clearSearchInput() }) if (checkEvt) { value = this.getCheckedNodes(info, props).map(n => { return { value: getValuePropValue(n), label: this.getLabelFromNode(n), } }) } else { if (value.some(i => i.value === selectedValue)) { return } value = value.concat([{ value: selectedValue, label: selectedLabel, }]) } } else { if (value.length && value[0].value === selectedValue) { this.setOpenState(false) return } value = [{ value: selectedValue, label: selectedLabel, }] this.setOpenState(false) } const extraInfo = { triggerValue: selectedValue, triggerNode: item, } if (checkEvt) { extraInfo.checked = info.checked // if inputValue existing, tree is checkStrictly extraInfo.allCheckedNodes = props.treeCheckStrictly || this.sInputValue ? info.checkedNodes : flatToHierarchy(info.checkedNodesPositions) this._checkedNodes = info.checkedNodesPositions const _tree = this.getPopupComponentRefs() this._treeNodesStates = _tree.checkKeys } else { extraInfo.selected = info.selected } this.fireChange(value, extraInfo) if (props.inputValue === null) { this.setState({ sInputValue: '', }) } }, onDeselect (info) { this.removeSelected(getValuePropValue(info.node)) if (!isMultiple(this.$props)) { this.setOpenState(false) } else { this.clearSearchInput() } }, onPlaceholderClick () { this.getInputDOMNode().focus() }, onClearSelection (event) { const props = this.$props const state = this.$data if (props.disabled) { return } event.stopPropagation() this._cacheTreeNodesStates = 'no' this._checkedNodes = [] if (state.sInputValue || state.sValue.length) { this.setOpenState(false) if (typeof props.inputValue === 'undefined') { this.setState({ sInputValue: '', }, () => { this.fireChange([]) }) } else { this.fireChange([]) } } }, onChoiceAnimationLeave () { this.forcePopupAlign() }, getLabelFromNode (child) { return getPropValue(child, this.$props.treeNodeLabelProp) }, getLabelFromProps (props, value) { if (value === undefined) { return null } let label = null loopAllChildren(this.renderedTreeData || props.children, item => { if (getValuePropValue(item) === value) { label = this.getLabelFromNode(item) } }) if (label === null) { return value } return label }, getDropdownContainer () { if (!this.dropdownContainer) { this.dropdownContainer = document.createElement('div') document.body.appendChild(this.dropdownContainer) } return this.dropdownContainer }, getSearchPlaceholderElement (hidden) { const props = this.$props let placeholder if (isMultiple(props)) { placeholder = getComponentFromProp(this, 'placeholder') || getComponentFromProp(this, 'searchPlaceholder') } else { placeholder = getComponentFromProp(this, 'searchPlaceholder') } if (placeholder) { return ( {placeholder} ) } return null }, getInputElement () { const { sInputValue } = this.$data const { prefixCls, disabled } = this.$props const multiple = isMultiple(this.$props) const inputListeners = { input: this.onInputChange, keydown: this.onInputKeyDown, } if (multiple) { inputListeners.blur = this.onBlur inputListeners.focus = this.onFocus } return ( {sInputValue} {isMultiple(this.$props) ? null : this.getSearchPlaceholderElement(!!sInputValue)} ) }, getInputDOMNode () { return this.$refs.inputInstance }, getPopupDOMNode () { return this.$refs.trigger.getPopupDOMNode() }, getPopupComponentRefs () { return this.$refs.trigger.getPopupEleRefs() }, getValue (_props, val, init = true) { let value = val // if inputValue existing, tree is checkStrictly const _strict = init === '__strict' || init && (this.sInputValue || this.inputValue !== _props.inputValue) if (_props.treeCheckable && (_props.treeCheckStrictly || _strict)) { this.halfCheckedValues = [] value = [] val.forEach(i => { if (!i.halfChecked) { value.push(i) } else { this.halfCheckedValues.push(i) } }) } // if (!(_props.treeCheckable && !_props.treeCheckStrictly)) { if (!_props.treeCheckable || _props.treeCheckable && (_props.treeCheckStrictly || _strict)) { return value } let checkedTreeNodes if (this._cachetreeData && this._cacheTreeNodesStates && this._checkedNodes && !this.sInputValue) { this.checkedTreeNodes = checkedTreeNodes = this._checkedNodes } else { /** * Note: `this._treeNodesStates`'s treeNodesStates must correspond to nodes of the * final tree (`processTreeNode` function from SelectTrigger.jsx produce the final tree). * * And, `this._treeNodesStates` from `onSelect` is previous value, * so it perhaps only have a few nodes, but the newly filtered tree can have many nodes, * thus, you cannot use previous _treeNodesStates. */ // getTreeNodesStates is not effective. this._treeNodesStates = getTreeNodesStates( this.renderedTreeData || _props.children, value.map(item => item.value) ) this.checkedTreeNodes = checkedTreeNodes = this._treeNodesStates.checkedNodes } const mapLabVal = arr => arr.map(itemObj => { return { value: getValuePropValue(itemObj.node), label: getPropValue(itemObj.node, _props.treeNodeLabelProp), } }) const props = this.$props let checkedValues = [] if (props.showCheckedStrategy === SHOW_ALL) { checkedValues = mapLabVal(checkedTreeNodes) } else if (props.showCheckedStrategy === SHOW_PARENT) { const posArr = filterParentPosition(checkedTreeNodes.map(itemObj => itemObj.pos)) checkedValues = mapLabVal(checkedTreeNodes.filter( itemObj => posArr.indexOf(itemObj.pos) !== -1 )) } else { checkedValues = mapLabVal(checkedTreeNodes.filter(itemObj => { return !itemObj.node.componentOptions.children })) } return checkedValues }, getCheckedNodes (info, props) { // TODO treeCheckable does not support tags/dynamic let { checkedNodes } = info // if inputValue existing, tree is checkStrictly if (props.treeCheckStrictly || this.sInputValue) { return checkedNodes } const checkedNodesPositions = info.checkedNodesPositions if (props.showCheckedStrategy === SHOW_ALL) { // checkedNodes = checkedNodes } else if (props.showCheckedStrategy === SHOW_PARENT) { const posArr = filterParentPosition(checkedNodesPositions.map(itemObj => itemObj.pos)) checkedNodes = checkedNodesPositions.filter(itemObj => posArr.indexOf(itemObj.pos) !== -1) .map(itemObj => itemObj.node) } else { checkedNodes = checkedNodes.filter(n => { return !n.componentOptions.children }) } return checkedNodes }, getDeselectedValue (selectedValue) { const checkedTreeNodes = this.checkedTreeNodes let unCheckPos checkedTreeNodes.forEach(itemObj => { const nodeProps = getAllProps(itemObj.node) if (nodeProps.value === selectedValue) { unCheckPos = itemObj.pos } }) const newVals = [] const newCkTns = [] checkedTreeNodes.forEach(itemObj => { if (isPositionPrefix(itemObj.pos, unCheckPos) || isPositionPrefix(unCheckPos, itemObj.pos)) { // Filter ancestral and children nodes when uncheck a node. return } const nodeProps = getAllProps(itemObj.node) newCkTns.push(itemObj) newVals.push(nodeProps.value) }) this.checkedTreeNodes = this._checkedNodes = newCkTns const nv = this.sValue.filter(val => newVals.indexOf(val.value) !== -1) this.fireChange(nv, { triggerValue: selectedValue, clear: true }) }, setOpenState (open, needFocus, documentClickClose = false) { this.clearDelayTimer() const { $props: props } = this // can not optimize, if children is empty // if (this.sOpen === open) { // return; // } if (!this.$props.dropdownVisibleChange(open, { documentClickClose })) { return } this.setState({ sOpen: open, }, () => { if (needFocus || open) { // Input dom init after first time component render // Add delay for this to get focus setTimeout(() => { if (open || isMultiple(props)) { const input = this.getInputDOMNode() if (input && document.activeElement !== input) { input.focus() } } else if (this.$refs.selection) { this.$refs.selection.focus() } }, 0) } }) }, clearSearchInput () { this.getInputDOMNode().focus() if (!hasProp(this, 'inputValue')) { this.setState({ sInputValue: '' }) } }, addLabelToValue (props, value_) { let value = value_ if (this.isLabelInValue()) { value.forEach((v, i) => { if (Object.prototype.toString.call(value[i]) !== '[object Object]') { value[i] = { value: '', label: '', } return } v.label = v.label || this.getLabelFromProps(props, v.value) }) } else { value = value.map(v => { return { value: v, label: this.getLabelFromProps(props, v), } }) } return value }, clearDelayTimer () { if (this.delayTimer) { clearTimeout(this.delayTimer) this.delayTimer = null } }, removeSelected (selectedVal, e) { const props = this.$props if (props.disabled) { return } // Do not trigger Trigger popup if (e && e.stopPropagation) { e.stopPropagation() } this._cacheTreeNodesStates = 'no' if (props.treeCheckable && (props.showCheckedStrategy === SHOW_ALL || props.showCheckedStrategy === SHOW_PARENT) && !(props.treeCheckStrictly || this.sInputValue)) { this.getDeselectedValue(selectedVal) return } // click the node's `x`(in select box), likely trigger the TreeNode's `unCheck` event, // cautiously, they are completely different, think about it, the tree may not render at first, // but the nodes in select box are ready. let label const value = this.sValue.filter((singleValue) => { if (singleValue.value === selectedVal) { label = singleValue.label } return (singleValue.value !== selectedVal) }) const canMultiple = isMultiple(props) if (canMultiple) { let event = selectedVal if (this.isLabelInValue()) { event = { value: selectedVal, label, } } this.__emit('deselect', event) } if (props.treeCheckable) { if (this.checkedTreeNodes && this.checkedTreeNodes.length) { this.checkedTreeNodes = this._checkedNodes = this.checkedTreeNodes.filter(item => { const nodeProps = getAllProps(item.node) return value.some(i => i.value === nodeProps.value) }) } } this.fireChange(value, { triggerValue: selectedVal, clear: true }) }, openIfHasChildren () { const props = this.$props if (props.children.length || (props.treeData && props.treeData.length) || !isMultiple(props)) { this.setOpenState(true) } }, fireChange (value, extraInfo = {}) { const props = getOptionProps(this) const vals = value.map(i => i.value) const sv = this.sValue.map(i => i.value) if (vals.length !== sv.length || !vals.every((val, index) => sv[index] === val)) { const ex = { preValue: [...this.sValue], ...extraInfo, } let labs = null let vls = value if (!this.isLabelInValue()) { labs = value.map(i => i.label) vls = vls.map(v => v.value) } else if (this.halfCheckedValues && this.halfCheckedValues.length) { this.halfCheckedValues.forEach(i => { if (!vls.some(v => v.value === i.value)) { vls.push(i) } }) } if (props.treeCheckable && ex.clear) { const treeData = this.renderedTreeData || props.children ex.allCheckedNodes = flatToHierarchy(filterAllCheckedData(vals, treeData)) } if (props.treeCheckable && this.sInputValue) { const _vls = [...this.sValue] if (ex.checked) { value.forEach(i => { if (_vls.every(ii => ii.value !== i.value)) { _vls.push({ ...i }) } }) } else { let index const includeVal = _vls.some((i, ind) => { if (i.value === ex.triggerValue) { index = ind return true } }) if (includeVal) { _vls.splice(index, 1) } } vls = _vls if (!this.isLabelInValue()) { labs = _vls.map(v => v.label) vls = _vls.map(v => v.value) } } this._savedValue = isMultiple(props) ? vls : vls[0] this.__emit('change', this._savedValue, labs, ex) if (!('value' in props)) { this._cacheTreeNodesStates = false this.setState({ sValue: this.getValue(props, toArray(this._savedValue).map((v, i) => { return this.isLabelInValue() ? v : { value: v, label: labs && labs[i], } })), }, this.forcePopupAlign) } } }, isLabelInValue () { const { treeCheckable, treeCheckStrictly, labelInValue } = this.$props if (treeCheckable && treeCheckStrictly) { return true } return labelInValue || false }, onFocus (e) { this.__emit('focus', e) }, onBlur (e) { this.__emit('blur', e) }, focus () { if (!isMultiple(this.$props)) { this.$refs.selection.focus() } else { this.getInputDOMNode().focus() } }, blur () { if (!isMultiple(this.$props)) { this.$refs.selection.blur() } else { this.getInputDOMNode().blur() } }, forcePopupAlign () { this.$refs.trigger.$refs.trigger.forcePopupAlign() }, renderTopControlNode () { const { sValue: value } = this.$data const props = this.$props const { choiceTransitionName, prefixCls, maxTagTextLength } = props const multiple = isMultiple(props) // single and not combobox, input is inside dropdown if (!multiple) { let innerNode = ( {getComponentFromProp(this, 'placeholder') || ''} ) if (value.length) { innerNode = ( {value[0].label} ) } return ( {innerNode} ) } const selectedValueNodes = value.map((singleValue) => { let content = singleValue.label const title = content if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) { content = `${content.slice(0, maxTagTextLength)}...` } return (