de/core/frontend/src/components/elTreeSelect/index.vue
2023-08-24 17:34:35 +08:00

632 lines
16 KiB
Vue

<template>
<div
class="el-tree-select"
:class="selectClass"
>
<el-select
:id="'el-tree-select-' + guid"
ref="select"
v-model="labels"
v-popover:popover
:style="styles"
:collapse-tags="showNumber"
class="el-tree-select-input"
:disabled="disabled"
popper-class="de-select-option"
v-bind="selectParams"
:popper-append-to-body="popperAppendToBody"
:filterable="false"
:multiple="selectParams.multiple"
:title="labels"
@remove-tag="_selectRemoveTag"
@clear="_selectClearFun"
@focus="_popoverShowFun"
/>
<el-popover
ref="popover"
v-model="visible"
:append-to-body="popperAppendToBody"
:placement="placement"
:transition="transition"
:popper-class="popperClass"
:width="width"
trigger="click"
@show="showPopover"
>
<el-input
v-if="treeParams.filterable"
ref="input"
v-model="keywords"
size="mini"
class="input-with-select mb10"
>
<el-button
slot="append"
icon="el-icon-search"
@click="_searchFun"
/>
</el-input>
<p
v-if="selectParams.multiple"
class="tree-select-all"
><el-checkbox
v-model="selectAll"
v-customStyle="customStyle"
:indeterminate="isIndeterminate"
@change="selectAllChange"
>{{ $t('dataset.check_all') }}</el-checkbox></p>
<el-scrollbar
tag="div"
wrap-class="el-select-dropdown__wrap"
view-class="el-select-dropdown__list"
class="is-empty"
>
<el-tree
v-show="data.length > 0"
ref="tree"
:popper-append-to-body="popperAppendToBody"
v-bind="treeParams"
:data="data"
:node-key="propsValue"
:draggable="false"
:current-node-key="ids.length > 0 ? ids[0] : ''"
:show-checkbox="selectParams.multiple"
:filter-node-method="filterNodeMethod ? filterNodeMethod : _filterFun"
:render-content="treeRenderFun"
@node-click="_treeNodeClickFun"
@check="_treeCheckFun"
@check-change="_treeCheckChange"
/>
<div
v-if="data.length === 0"
class="no-data"
>暂无数据</div>
</el-scrollbar>
</el-popover>
</div>
</template>
<script>
import { on, off } from './dom'
import { each, guid } from './utils'
export default {
name: 'ElTreeSelect',
components: {},
props: {
value: {
type: [String, Array, Number],
default() {
return ''
}
},
popperAppendToBody: {
type: Boolean,
default: false
},
styles: {
type: Object,
default() {
return {
width: '100%'
}
}
},
selectClass: {
type: String,
default() {
return ''
}
},
popoverClass: {
type: String,
default() {
return ''
}
},
disabled: {
type: Boolean,
default() {
return false
}
},
placement: {
type: String,
default() {
return 'bottom'
}
},
transition: {
type: String,
default() {
return 'el-zoom-in-top'
}
},
// eslint-disable-next-line vue/require-default-prop
treeRenderFun: Function,
// eslint-disable-next-line vue/require-default-prop
filterNodeMethod: Function,
selectParams: {
type: Object,
default() {
return {
clearable: true,
disabled: false,
placeholder: '请选择'
}
}
},
treeParams: {
type: Object,
default() {
return {
clickParent: false,
filterable: false,
leafOnly: false,
includeHalfChecked: false,
data: [],
showParent: false,
props: {
children: 'children',
label: 'name',
code: 'code',
value: 'flowId',
disabled: 'disabled'
}
}
}
},
customStyle: {
type: Object,
default: () => {}
}
},
data() {
return {
selectAll: false,
guid: guid(),
propsValue: 'flowId',
propsLabel: 'name',
propsCode: null,
propsDisabled: 'disabled',
propsChildren: 'children',
leafOnly: false,
includeHalfChecked: false,
data: [],
keywords: '',
labels: '',
ids: [],
visible: false,
width: 150,
showParent: false,
showNumber: false
}
},
computed: {
popperClass() {
const _c = 'el-tree-select-popper ' + this.popoverClass
return this.disabled ? _c + ' disabled ' : _c
},
isIndeterminate() {
if (!this.selectParams.multiple) return
return this.ids.length > 0 && this.ids.length !== this._checkSum().length
}
},
watch: {
ids: function(val) {
if (val !== undefined) {
this.$nextTick(() => {
this._setSelectNodeFun(val)
})
}
},
value: function(val) {
if (this.ids !== val) {
this._setMultipleFun()
if (this.selectParams.multiple) {
this.ids = [...val]
} else {
this.ids = val === '' ? [] : [val]
}
}
},
labels: function() {
this.setShowNumber()
}
},
created() {
const { props, data, leafOnly, includeHalfChecked, showParent } = this.treeParams
this._setMultipleFun()
this.propsValue = props.value
this.propsLabel = props.label
this.propsCode = props.code || null
this.propsDisabled = props.disabled
this.propsChildren = props.children
this.leafOnly = leafOnly
this.includeHalfChecked = includeHalfChecked
this.data = data.length > 0 ? [...data] : []
if (this.selectParams.multiple) {
this.labels = []
this.ids = this.value
} else {
this.labels = ''
this.ids = this.value instanceof Array ? this.value : [this.value]
}
this.showParent = showParent
},
mounted() {
this._updateH()
this.$nextTick(() => {
on(document, 'mouseup', this._popoverHideFun)
this.bindScroll()
})
},
beforeDestroy() {
off(document, 'mouseup', this._popoverHideFun)
this.unbindScroll()
},
methods: {
bindScroll() {
window.onmousewheel = this._popoverHideFun
on(document, 'DOMMouseScroll', this._popoverHideFun)
},
unbindScroll() {
window.onmousewheel = null
off(document, 'DOMMouseScroll', this._popoverHideFun)
},
showPopover() {
this.$nextTick(() => {
this.$refs.input.focus()
})
},
resetSelectAll() {
this.selectAll = false
},
selectAllChange(val) {
if (val) {
this.ids = this._checkSum()
this._emitFun()
this.$emit('check', null, this.ids, null)
return
}
this._selectClearFun()
},
_checkSum() {
let arr = [];
(this.data || []).forEach(ele => {
arr = [...this.allKidIds(ele), ...arr]
})
return arr
},
_treeCheckChange() {
this.$emit('treeCheckChange')
},
_setMultipleFun() {
let multiple = false
if (this.value instanceof Array) {
multiple = true
}
this.$set(this.selectParams, 'multiple', multiple)
},
_searchFun() {
this.$emit('searchFun', this.keywords)
},
_setSelectNodeFun(ids) {
const el = this.$refs.tree
if (!el) {
throw new Error('找不到tree dom')
}
const { multiple } = this.selectParams
if (ids.length === 0 || this.data.length === 0) {
this.labels = multiple ? [] : ''
if (multiple) {
el.setCheckedKeys([])
} else {
el.setCurrentKey(null)
}
return
}
if (multiple) {
el.getCheckedNodes(this.leafOnly, this.includeHalfChecked).forEach(item => {
el.setChecked(item, false)
})
ids.forEach(id => {
el.setChecked(id, true)
})
const nodes = el.getCheckedNodes(this.leafOnly, this.includeHalfChecked)
if (!this.showParent) {
if (this.propsCode) {
this.labels = nodes.map(item => (item[this.propsCode] ? item[this.propsLabel] + '(' + item[this.propsCode] + ')' : item[this.propsLabel])) || []
} else {
this.labels = nodes.map(item => item[this.propsLabel]) || []
}
} else {
this.labels = nodes.map(item => this.cascadeLabels(item)) || []
}
} else {
el.setCurrentKey(ids[0])
const node = el.getCurrentNode()
if (node) {
if (!this.showParent) {
if (this.propsCode) {
this.labels = node[this.propsCode] ? node[this.propsLabel] + '(' + node[this.propsCode] + ')' : node[this.propsLabel]
} else {
this.labels = node[this.propsLabel]
}
} else {
this.labels = this.cascadeLabels(node)
}
} else {
this.labels = ''
}
}
this._updatePopoverLocationFun()
},
parentNodes(node) {
const results = []
let currentNode = node
while (currentNode && currentNode.data && !(currentNode.data instanceof Array)) {
results.push(currentNode)
currentNode = currentNode.parent
}
return results
},
cascadeLabels(data) {
const cNode = this.$refs.tree.getNode(data)
const linkedNodes = this.parentNodes(cNode)
const labels = linkedNodes.map(item => item.data[this.propsLabel]).reverse().join(':')
return labels
},
_updatePopoverLocationFun() {
setTimeout(() => {
this.$refs.popover.updatePopper()
}, 50)
},
_getEventPath(evt) {
const path = (evt.composedPath && evt.composedPath()) || evt.path
const target = evt.target
if (path != null) {
return path.indexOf(window) < 0 ? path.concat(window) : path
}
if (target === window) {
return [window]
}
function getParents(node, memo) {
memo = memo || []
const parentNode = node.parentNode
if (!parentNode) {
return memo
} else {
return getParents(parentNode, memo.concat(parentNode))
}
}
return [target].concat(getParents(target), window)
},
_filterFun(value, data, node) {
if (!value) return true
return data[this.propsLabel?.toLocaleUpperCase()].indexOf(value.toLocaleUpperCase()) !== -1
},
_treeNodeClickFun(data, node, vm) {
const { multiple } = this.selectParams
if (multiple) return
const { clickParent } = this.treeParams
const checkStrictly = this.treeParams['check-strictly']
const { propsValue, propsChildren, propsDisabled } = this
const children = data[propsChildren] || []
if (data[propsDisabled]) {
return
}
if (node.checked) {
const value = data[propsValue]
this.ids = this.ids.filter(id => id !== value)
if (!checkStrictly && children.length) {
children.forEach(item => {
this.ids = this.ids.filter(id => id !== item[propsValue])
})
}
} else {
if (!multiple) {
if (!clickParent) {
if (children.length === 0) {
this.ids = [data[propsValue]]
this.visible = false
} else {
return false
}
} else {
this.ids = [data[propsValue]]
this.visible = false
}
} else {
if (!clickParent && children.length === 0) {
this.ids.push(data[propsValue])
} else if (clickParent) {
this.ids.push(data[propsValue])
if (!checkStrictly && children.length) {
children.forEach(item => {
this.ids.push(item[propsValue])
})
}
}
}
}
this._emitFun()
this.$emit('node-click', data, node, vm)
},
_treeCheckFun(data, node, vm) {
this.ids = []
const { propsValue } = this
const checkKeys = this.$refs.tree.getCheckedKeys()
checkKeys.forEach((i, n) => {
const node = this.$refs.tree.getNode(i)
if (!node.visible && node.checked) {
this.$refs.tree.setChecked(i, false)
}
})
const checkedNodes = this.$refs.tree.getCheckedNodes()
checkedNodes.forEach(item => {
this.ids.push(item[propsValue])
})
node.checkedKeys = checkedNodes.map(node => node.id)
this.selectAll = this._checkSum().length === this.ids.length
this.$emit('check', data, node, vm)
this._emitFun()
},
allKidIds(node, ids) {
ids = ids || []
if (!node) {
return
}
const stack = []
stack.push(node)
let tmpNode
while (stack.length > 0) {
tmpNode = stack.pop()
ids.push(tmpNode.id)
if (tmpNode.children && tmpNode.children.length > 0) {
var i = tmpNode.children.length - 1
for (i = tmpNode.children.length - 1; i >= 0; i--) {
stack.push(tmpNode.children[i])
}
}
}
return ids
},
_selectRemoveTag(tag) {
const { data, propsValue, propsLabel, propsChildren } = this
const { multiple } = this.selectParams
each(
data,
item => {
const labels = this.showParent ? this.cascadeLabels(item) : item[propsLabel]
if (labels === tag) {
if (multiple && item.children && item.children.length) {
const needCancelIds = this.allKidIds(item) || []
this.ids = this.ids.filter(id => !needCancelIds.includes(id))
} else {
const value = item[propsValue]
this.ids = this.ids.filter(id => id !== value)
}
}
},
propsChildren
)
this.$refs.tree.setCheckedKeys(this.ids)
this.$emit('removeTag', this.ids, tag)
this._emitFun()
},
_selectClearFun() {
this.ids = []
const { multiple } = this.selectParams
this.$emit('input', multiple ? [] : '')
this.$emit('select-clear')
this.selectAll = false
this._updatePopoverLocationFun()
},
_emitFun() {
const { multiple } = this.selectParams
this.$emit('input', multiple ? this.ids : this.ids.length > 0 ? this.ids[0] : '')
this._updatePopoverLocationFun()
},
_updateH() {
this.$nextTick(() => {
this.width = this.$refs.select.$el.getBoundingClientRect().width
})
},
_popoverShowFun(val) {
this._updateH()
this.$emit('onFocus')
},
_popoverHideFun(e) {
const path = this._getEventPath(e)
const isInside = path.some(list => {
return list.className && typeof list.className === 'string' && list.className.indexOf('el-tree-select') !== -1
})
if (!isInside) {
this.visible = false
}
},
treeDataUpdateFun(data) {
this.data = data
if (data.length > 0) {
setTimeout(() => {
this._setSelectNodeFun(this.ids)
}, 300)
}
},
filterFun(val) {
this.$refs.tree.filter(val)
},
setShowNumber() {
this.showNumber = false
this.$nextTick(() => {
if (!this.selectParams.multiple || !this.$refs.select || !this.$refs.select.$refs.tags) {
return
}
const kids = this.$refs.select.$refs.tags.children[0].children
let contentWidth = 0
kids.forEach(kid => {
contentWidth += kid.offsetWidth
})
this.showNumber = contentWidth > ((this.$refs.select.$refs.tags.clientWidth - 35) * 0.9)
})
}
}
}
</script>
<style>
.el-tree-select .de-select-option {
display: none !important;
}
.tree-select-all {
padding: 10px 20px 0 24px;
}
[aria-disabled='true'] > .el-tree-node__content {
color: inherit !important;
background: transparent !important;
cursor: no-drop !important;
}
.el-tree-select-popper {
max-height: 400px;
overflow: auto;
}
.el-tree-select-popper.disabled {
display: none !important;
}
.el-tree-select-popper .el-button--small {
width: 25px !important;
min-width: 25px !important;
}
.el-tree-select-popper[x-placement^='bottom'] {
margin-top: 5px;
}
.mb10 {
margin-bottom: 10px;
}
.no-data {
height: 32px;
line-height: 32px;
font-size: 14px;
color: #cccccc;
text-align: center;
}
</style>