kui-vue
Version:
A high quality UI Toolkit built on Vue.js 2.0
509 lines (472 loc) • 15.4 kB
JSX
import Icon from "../icon";
import Empty from '../empty';
import { hasProp, getChild, isNotEmpty } from '../_tool/utils'
import Drop from '../base/drop'
import { t } from '../locale';
import { Sync, Close, CloseCircle, ChevronDown } from 'kui-icons'
import { Tree } from '../tree'
const cloneDeep = require('lodash.clonedeep');
// const cloneDeep = require('lodash/cloneDeep')
export default {
name: "TreeSelect",
props: {
placeholder: String,
size: {
default: 'default',
validator(value) {
return ["small", "large", "default"].indexOf(value) >= 0;
}
},
transfer: { type: Boolean, default: true },
width: Number,
value: [String, Number, Array],
clearable: Boolean,
filterable: Boolean,
disabled: Boolean,
multiple: Boolean,
loading: Boolean,
bordered: { type: Boolean, default: true },
showArrow: { type: Boolean, default: true },
theme: String,
emptyText: String,
icon: [String, Array],
shape: String,
arrowIcon: [String, Array],
treeData: Array,
treeCheckable: Boolean,
treeShowLine: Boolean,
treeShowIcon: { type: Boolean, default: true },
treeCheckStrictly: Boolean,
treeExpandedKeys: Array,
treeExpandedAll: Boolean,
},
provide() {
return {
Select: this
}
},
data() {
return {
label: "",
opened: false,
currentValue: this.value || this.multiple ? [] : '',
showSearch: false,
queryKey: '',
selectWidth: this.width,
isFocus: false
};
},
watch: {
value(value) {
if (isNotEmpty(value)) {
this.currentValue = value
} else {
this.currentValue = this.multiple ? [] : ''
}
},
currentValue(n, o) {
this.setLabel()
}
},
methods: {
hideDrop() {
if (this.showSearch) {
this.queryKey = ''
this.$refs.search.value = ''
this.$refs.search.style.width = ''
}
this.showSearch = false
},
findNodeByKeyIterative(tree, key) {
//迭代搜索(深度优先搜索,DFS) 当树的深度可能导致递归方法栈溢出时。
const stack = [...tree];
while (stack.length) {
const node = stack.pop();
if (node.key === key) {
return node;
}
if (node.children) {
stack.push(...node.children);
}
}
return null;
},
filterTreeByKeyword(tree, keyword) {
// 使用 lodash 的 cloneDeep 函数深度拷贝树
return cloneDeep(tree).filter(function iter(node) {
// 使用 _.some 遍历判断子节点是否符合条件
if (node.title.includes(keyword)) return true;
if (node.children) node.children = node.children.filter(iter);
return node.children && node.children.length > 0;
});
// // 创建一个栈来迭代树,存储节点及其父节点信息
// const stack = tree.map(node => ({ node, parent: null }));
// const result = [];
// // 递归函数,克隆并添加节点及其所有子节点
// function addNodeWithChildren(node) {
// // 深拷贝节点以防止原树被修改
// const newNode = { ...node, children: [] };
// if (node.children) {
// for (const child of node.children) {
// newNode.children.push(addNodeWithChildren(child));
// }
// }
// return newNode;
// }
// // 存储节点的所有父节点,确保结果包含父节点链
// const parentMap = new Map();
// // 开始迭代树
// while (stack.length > 0) {
// const { node, parent } = stack.pop();
// // 如果节点名包含关键字
// if (node.name.includes(keyword)) {
// // 把该节点的父节点链添加到结果中
// let currentNode = parent;
// while (currentNode) {
// if (!parentMap.has(currentNode.key)) {
// parentMap.set(currentNode.key, addNodeWithChildren(currentNode));
// result.push(parentMap.get(currentNode.key));
// }
// currentNode = parentMap.get(currentNode.key);
// }
// // 将当前节点及其子节点添加到结果中
// result.push(addNodeWithChildren(node));
// }
// // 把子节点加入堆栈继续遍历
// if (node.children) {
// for (const child of node.children) {
// stack.push({ node: child, parent: node });
// }
// }
// }
// return result;
},
getLabel(childs, labelValue) {
let node = this.findNodeByKeyIterative(childs, labelValue)
return node?.title || labelValue;
},
setLabel() {
let { currentValue, multiple, label } = this
currentValue = isNotEmpty(currentValue) ? currentValue : (multiple ? [] : '')
let currentLabel = isNotEmpty(label) ? label : (multiple ? [] : '')
if (multiple) {
if (currentValue.length) {
let labels = []
currentValue.forEach((value) => {
let label = this.getLabel(this.treeData, value)
labels.push({ label, key: `label_${value}`, value })
})
currentLabel = labels
} else {
currentLabel = []
}
} else {
currentLabel = this.getLabel(this.treeData, currentValue)
}
this.label = currentLabel
setTimeout(e => { this.setPosition() }, 230);
},
clear(e) {
let label = this.multiple ? [] : ""
let value = this.multiple ? [] : ""
this.label = label
this.currentValue = value
this.$emit("input", value);
this.$emit("change", value);
e.stopPropagation()
},
showDrops() {
let isSearch = ('search' in this.$listeners)
this.opened = !this.opened;
if (this.filterable || isSearch) {
this.showSearch = this.opened
if (this.opened) {
this.$nextTick(e => {
this.isFocus = true
this.$refs.search.focus()
})
} else {
this.$refs.search.blur()
this.isFocus = false
setTimeout(() => {
this.queryKey = ''
this.$refs.search.value = ''
}, 200);
}
}
// this.$nextTick(e => this.setPosition())
},
toggleDrop() {
if (this.disabled) {
return false;
}
let isSearch = ('search' in this.$listeners)
if (isSearch) {
// this.$nextTick(e => {
this.showSearch = true
this.$nextTick(e => {
this.$refs.search.focus()
this.isFocus = true
})
// })
return;
}
this.showDrops()
},
setPosition() {
// if (!hasProp(this, 'width')) {
// this.selectWidth = this.$el.offsetWidth
// }
if (this.opened) {
this.$refs.overlay.setPosition()
}
},
select({ selectedKeys, selected, node }) {
let item = { label: node.title, value: node.key }
let { multiple, value, currentValue } = this
// console.log(value, currentValue, item)
if (this.showSearch) {
this.queryKey = ''
this.$refs.search.value = ''
this.$refs.search.style.width = ''
}
if (!multiple) {
this.opened = false
this.showSearch = false
} else if ('search' in this.$listeners || this.filterable) {
this.$nextTick(e => {
this.$refs.search.focus()
this.isFocus = true
})
}
let hasValue = hasProp(this, 'value')
//set value
let newValue = value
if (!hasValue) {
newValue = this.multiple ? currentValue || [] : item.value
this.currentValue = newValue
}
if (!multiple) {
this.currentValue = item.value
this.currentLabel = item.title
}
//set label
if (!hasValue) {
if (multiple) {
let currentLabel = this.label || []
let index = currentLabel.findIndex(x => x.value === item.value)// .map(x => x.label).indexOf(item.label)
if (index === -1) {
currentLabel.push({ label: item.label, key: item.label + item.value, value: item.value })
} else {
currentLabel.splice(index, 1)
}
this.label = currentLabel
} else {
this.label = item.label
}
setTimeout(e => { this.setPosition() }, 230);
} else {
this.$nextTick(e => this.setPosition())
}
this.$emit("input", this.currentValue);
this.$emit("change", this.currentValue);
this.$emit('select', {
selectedKeys,
selected,
node
})
},
removeTag(e, i) {
if (this.disabled) return
// let values = this.currentValue || []
// let labels = this.label || []
// this.change({ value: values[i], label: labels[i].label })
this.currentValue.splice(i, 1)
this.$emit("input", this.currentValue);
// console.log(this.currentValue,this.label)
this.$emit("change", this.currentValue);
e.stopPropagation()
},
searchInput(e) {
this.queryKey = e.target.value
//todo:
this.$nextTick(k => {
// let max = this.selectWidth - 15 - (this.showArrow ? 25 : 0)
e.target.style.width = this.$refs.mirror.offsetWidth + 'px'
this.setPosition()
})
if ('search' in this.$listeners) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.opened = true;
this.$emit('search', e)
}, 500);
}
},
emptyClick(e) {
if (this.showSearch) {
this.$nextTick(e => {
this.$refs.search.focus()
this.isFocus = true
})
}
},
treeCheck() {
// console.log(e)
// console.log(checkedKeys)
}
},
mounted() {
if (hasProp(this, 'value'))
this.setLabel()
},
render() {
let { disabled, size, multiple,
opened, placeholder, showArrow, bordered,
clear, removeTag, queryKey, theme, arrowIcon, icon, shape, filterable,
clearable, label, toggleDrop, isFocus, currentValue,
treeData, treeCheckable, treeShowLine, treeShowIcon, treeCheckStrictly, treeExpandedKeys, treeExpandedAll
} = this
let childNode = []
if (arrowIcon == undefined) {
arrowIcon = ChevronDown
}
const classes = [
"k-tree-select",
{
"k-tree-select-disabled": disabled,
"k-tree-select-open": opened,
'k-tree-select-borderless': bordered === false,
"k-tree-select-lg": size == 'large',
"k-tree-select-sm": size == 'small',
"k-tree-select-light": theme == 'light',
"k-tree-select-has-icon": !!icon,
"k-tree-select-circle": shape == 'circle' && !multiple,
"k-tree-select-multiple": multiple,
"k-tree-select-show-search": isFocus,
"k-tree-select-show-tags": multiple && (label || []).length
}
]
const queryProps = {
on: {
input: this.searchInput,
blur: () => {
if (!this.opened) this.showSearch = false
this.isFocus = false
}
},
ref: 'search',
class: 'k-tree-select-search',
attrs: {
autoComplete: 'off',
}
}
const queryNode = <div v-show={this.showSearch} key="search" class="k-tree-select-search-wrap">
<input {...queryProps} />
<span class="k-tree-select-search-mirror" ref="mirror">{queryKey}</span>
</div>
const loadingNode = <div class="k-tree-select-loading"><Icon type={Sync} spin /><span>{t('k.select.loading')}</span></div>
const props = {
ref: 'overlay',
props: {
width: this.selectWidth,
value: opened,
selection: this.$el,
transfer: true,
extendWidth: true,
transitionName: 'k-tree-select',
className: ['k-tree-select-dropdown', { 'k-tree-select-dropdown-multiple': this.multiple, 'k-tree-select-dropdown-sm': size == 'small' }]
},
on: {
input: e => {
this.opened = e
},
hide: () => {
this.opened = false
setTimeout(() => {
this.hideDrop()
}, 300)
}
}
}
const selectedKeys = this.multiple ? currentValue || [] : [currentValue]
// console.log(this.$listeners)
const treeProps = {
props: {
data: treeData,
checkable: treeCheckable,
multiple: multiple,
showLine: treeShowLine,
showIcon: treeShowIcon,
checkStrictly: treeCheckStrictly,
expandedKeys: treeExpandedKeys,
selectedKeys: selectedKeys,
checkedKeys: selectedKeys,
},
on: {
select: this.select,
check: this.treeCheck
}
}
if ('tree-load-data' in this.$listeners) {
treeProps.on['load-data'] = this.$listeners['tree-load-data']
}
if (this.filterable && queryKey && !this.$listeners.search) {
let parsedQuery = String(queryKey).replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, "\\$1");
treeProps.props.data = this.filterTreeByKeyword(treeData, parsedQuery)
}
let overlay = <Drop {...props}>
{this.loading ? loadingNode : (!(treeProps.props.data && treeProps.props.data.length) ?
<Empty onClick={this.emptyClick} description={this.emptyText} /> :
<Tree {...treeProps} />)}
</Drop>
label = multiple ? (label || []) : label + ''
placeholder = placeholder || t('k.select.placeholder')
const placeNode = ((placeholder && (((label === null || label === undefined) || !label.length) && !queryKey))
? <div class="k-tree-select-placeholder">{placeholder}</div>
: null
)
const tags = multiple ?
label.map((c, i) => {
return <span class="k-tree-select-tag" key={c.key}>{c.label}<Icon type={Close} onClick={e => removeTag(e, i)} /></span>
})
: null
const labelStyle = {
// opacity: this.showSearch ? .4 : 1,
display: queryKey.length ? 'none' : ''
}
const labelsNode = ((multiple)
? (
[<div class="k-tree-select-labels" name="k-tree-select-tag">{tags}{queryNode}</div>]
)
: (label.length ? <div class="k-tree-select-label" style={labelStyle}>{label}</div> : null)
)
let isSearch = ('search' in this.$listeners)
childNode.push(labelsNode);
placeNode && childNode.push(placeNode);
if ((filterable || isSearch) && !multiple) {
childNode.push(queryNode)
}
// label = "1"
const styles = { width: `${this.width}px` }
let showClear = !disabled && clearable && isNotEmpty(label) && label.length > 0
classes[1]['k-tree-select-has-clear'] = showClear
const iconNodes = []
if (!isSearch && showArrow) {
iconNodes.push(<Icon class="k-tree-select-arrow" type={arrowIcon} />)
}
if (showClear) {
iconNodes.push(<Icon class="k-tree-select-clearable" type={CloseCircle} onClick={clear} />)
}
// console.log(iconNodes)
iconNodes.push(" ")
return (
<div tabIndex="0" class={classes} style={styles} onClick={toggleDrop} ref="rel">
{icon ? <Icon type={icon} class="k-tree-select-icon" /> : null}
<div class="k-tree-select-selection">{childNode}</div>
{iconNodes}
{overlay}
</div >
)
}
};