共计 14325 个字符,预计需要花费 36 分钟才能阅读完成。
<tag-select
allowLoadChildren
:options="organOptions"
:defaultOptions="organDefaultOptions"
v-model="form.organId"
placeholder="抉择组织机构"
@change="ispChange"
@spread="spread"
@focus="focus"
ref="tagSelect"
size="default"
:loadChildrenMethod="loadChildren"
labelKey="organName"
valueKey="organId"
childrenKey="sub"
:panelWidth="'auto'"
></tag-select>
<template>
<div class="cascader-wrapper">
<crec-popover
placement="bottomLeft"
trigger="click"
:popper-class="popOverClass"
v-model="showPopover"
>
<!-- <div slot="reference"> -->
<!-- multiple
mode="tags" -->
<crec-select
mode="multiple"
v-model="selectedLabels"
:default-value="defaultLabels"
:placeholder="placeholder"
:disabled="disabled"
:size="size"
:collapse-tags="collapseTags"
:showArrow="true"
style="width: 100%;"
popper-class="hide-popper"
:open="false"
@focus="handleFocus"
@deselect="removeTag"
@dropdown-visible-change="visibleChange"
></crec-select>
<!-- </div> -->
<template slot="content">
<div
class="cascader-menu-wrapper"
v-clickoutside="hidePopover"
>
<template v-if="options.length > 0">
<ul
class="crec-cascader-menu cascader-menu"
:style="{'width': panelWidth ==='auto'?'auto': panelWidth +'px'}"
v-for="(cas, index) in casTree"
:key="index"
>
<!-- -->
<!-- 'can-load-children': !item.isLeaf && !item[childrenKey] && allowLoadChildren && showLoadingIndicator, -->
<!-- 'loading-children': !item.isLeaf && item.loading && allowLoadChildren && showLoadingIndicator, -->
<!-- 'crec-cascader-menu-item-expand': item[childrenKey] && item[childrenKey].length > 0, -->
<li
:class="{'crec-cascader-menu-item': true,'crec-cascader-menu-item-expand': true,'has-checked-child': item.indeterminate || item.hasCheckedChild,'crec-cascader-menu-item-active': item.checked,}"@click="spreadNext(item[childrenKey], index, item)"v-for="(item, itemIdx) in cas":key="itemIdx":title="item[labelKey]"
>
<crec-checkbox
class="cascader-checkbox"
@click.native.stop
:disabled="item.disabled"
v-model="item.checked"
:indeterminate="item.indeterminate"
@change="({target}) => {checkedChange(item, target.checked) }"
></crec-checkbox>
<span>{{item[labelKey] }}</span>
<span
v-if="item[childrenKey] && item[childrenKey].length > 0"
class="crec-cascader-menu-item-expand-icon"
>
<crec-icon type="right"></crec-icon>
</span>
</li>
</ul>
</template>
<template v-else>
<ul class="crec-cascader-menu cascader-menu">
<li class="crec-cascader-menu-item dropdown__empty">
{{noDataText}}
</li>
</ul>
</template>
</div>
</template>
</crec-popover>
</div>
</template>
<script>
import Clickoutside from './clickoutside'
import {props, hasArrayChild, deepClone, getId, fireEvent, isPromise} from './utils'
export default {
name: 'TagSelect',
props,
watch: {
selectedLabels: {
deep: true,
handler () {if (this.selectedLabels && this.selectedLabels.length === 0) {this.selectedLabels = this.defaultLabels}
}
},
options: {
deep: true,
handler () {this.initOpts()
this.initDatas()}
},
defaultOptions: {
deep: true,
handler () {console.log(this.defaultOptions)
this.defaultLabels = this.defaultOptions.map(e => e.organName)
}
},
value: {
deep: true,
handler () {console.log(this.selectedValues, this.value)
if (this.selectedValues !== this.value) {this.initOpts()
this.initDatas()}
}
},
disabled (disabled) {if (disabled) {this.hidePopover()
}
},
showPopover (flag) {if (flag) {console.log(this.value)
this.$emit('focus')
}
}
},
directives: {Clickoutside},
created () {this.classRef = `popper-class-${getId()}`
this.popOverClass = `cascader-popper ${this.classRef} ${this.popperClass}`
this.initOpts()
this.initDatas()},
mounted () {
// 设置弹出层宽度
this.elWidth = this.$el.offsetWidth
},
destroyed () {
this.clonedOpts = null
this.casTree = null
this.selectedItems = null
this.selectedLabels = null
this.selectedvalues = null
},
data () {
return {
elWidth: '',
popperWidth: '',
popOverClass: '',
classRef: '',
showPopover: false,
clonedOpts: [],
casTree: [],
selectedItems: [],
selectedLabels: [],
selectedValues: [],
loadChildrenPromise: null,
defaultLabels: []}
},
methods: {initOpts () {console.log(this.defaultOptions)
this.defaultLabels = this.defaultOptions.map(e => e.organName)
console.log(this.defaultLabels)
this.clonedOpts = deepClone(this.options)
this.recursiveOpt(this.clonedOpts, null)
this.casTree = [this.clonedOpts]
console.log(this.casTree)
},
/**
* 初始化数据
* 空值初始化,两个绑定不统一的状况
*/
initDatas () {this.pickCheckedItem(this.clonedOpts)
},
/**
* 递归 option 数据
* 标记数据树形层级 parent
* 打上初始状态 checked indeterminate
*/
recursiveOpt (nodeArr, parent) {
const vm = this
nodeArr.forEach(node => {if (parent) {node.parent = parent}
node.indeterminate = false
node.checked = false
if (this.value.some(val => val === this.getLevel(node, vm.valueKey, this.outputLevelValue))) {node.checked = true}
this.markChildrenChecked(node)
this.markParentChecked(node)
this.markParentHasCheckChild(node)
if (hasArrayChild(node, vm.childrenKey)) {vm.recursiveOpt(node[vm.childrenKey], node)
}
})
},
/**
* 依据以后节点 checked
* 更改所有子孙节点 checked
* 依赖 this.selectChildren
*/
markChildrenChecked (node) {
const vm = this
function loop (children, status) {if (children) {
children.map(child => {if (!child.disabled) {
child.checked = status
if (child.checked) {child.indeterminate = false}
}
if (hasArrayChild(child, vm.childrenKey)) {loop(child[vm.childrenKey], status)
}
})
}
}
if (node && hasArrayChild(node, vm.childrenKey) && this.selectChildren) {loop(node[vm.childrenKey], node.checked)
}
},
/**
* 标记父节点 checked、indeterminate 状态
* 依赖 this.selectChildren
*/
markParentChecked (node) {
const vm = this
node.indeterminate = false
function loop (node) {
let checkCount = 0
if (hasArrayChild(node, vm.childrenKey)) {const childIndeterminate = node[vm.childrenKey].some(child => child.indeterminate)
node[vm.childrenKey].map(child => {if (child.checked) {checkCount++}
})
// 子节点全副被选中
if (checkCount === node[vm.childrenKey].length) {
node.checked = true
node.indeterminate = false
} else {
node.checked = false
if (checkCount > 0 || childIndeterminate) {node.indeterminate = true} else {node.indeterminate = false}
}
}
if (node.parent) {loop(node.parent)
}
}
if (node && node.parent && this.selectChildren) {loop(node.parent)
}
},
/**
* 标记是否有被选子项
* 依赖 this.selectChildren
*/
markParentHasCheckChild (node) {
const vm = this
node.hasCheckedChild = false
function loop (node) {
let checkCount = 0
if (hasArrayChild(node, vm.childrenKey)) {const childHasCheckedChild = node[vm.childrenKey].some(child => child.hasCheckedChild)
node[vm.childrenKey].map(child => {if (child.checked) {checkCount++}
})
// 子节点有被选中
node.hasCheckedChild = (checkCount > 0) || childHasCheckedChild
}
if (node.parent) {loop(node.parent)
}
}
if (node && node.parent && !this.selectChildren) {loop(node.parent)
}
},
// 展现标签所有层级
getLevel (node, key, leveled) {const levels = []
function loop (data) {levels.push(data[key])
if (data.parent) {loop(data.parent)
}
}
if (leveled) {loop(node)
return levels.reverse().join(this.separator)
} else {return node[key]
}
},
/**
* 解决已选中
* 从新遍历 tree,pick 除已选中我的项目
*/
pickCheckedItem (tree) {
const vm = this
/**
* 移除 parent 援用
*/
function removeParent (node) {const obj = {}
Object.keys(node).forEach(key => {if (key !== 'parent') {obj[key] = node[key]
}
})
if (hasArrayChild(obj, vm.childrenKey)) {obj[vm.childrenKey] = obj[vm.childrenKey].map(child => {return removeParent(child)
})
}
return obj
}
vm.selectedItems = []
vm.selectedLabels = []
vm.selectedValues = []
function loop (data) {if (Array.isArray(data)) {
data.map(item => {if (item.checked) {const newItem = removeParent(item)
vm.selectedItems.push(newItem)
vm.selectedLabels.push(vm.getLevel(item, vm.labelKey, vm.showAllLevels))
vm.selectedValues.push(vm.getLevel(item, vm.valueKey, vm.outputLevelValue))
}
if (hasArrayChild(item, vm.childrenKey)) {loop(item[vm.childrenKey])
}
})
}
}
loop(tree)
},
removeTag (label) {
/**
* 遍历 tree
* 依据传入 label 寻找 item
*/
const vm = this
function findNodeByLabel (label) {
let result = null
function loop (tree) {if (tree) {
tree.find(node => {if (vm.getLevel(node, vm.labelKey, vm.showAllLevels) === label) {
result = node
return true
}
if (hasArrayChild(node, vm.childrenKey)) {loop(node[vm.childrenKey])
}
})
}
}
if (label) {loop(vm.clonedOpts)
return result
}
}
const deletedItem = findNodeByLabel(label)
if (deletedItem) {vm.checkedChange(deletedItem, false)
}
this.$emit('remove-tag', label, deletedItem)
},
clearTag () {
const vm = this
function loop (nodeArr) {
nodeArr.forEach(node => {
node.checked = false
node.indeterminate = false
if (hasArrayChild(node, vm.childrenKey)) {loop(node[vm.childrenKey])
}
})
}
// 敞开全副状态
loop(this.clonedOpts)
this.selectedLabels = []
this.selectedValues = []
this.selectedItems = []
this.$emit('clear')
this.syncData()},
// 菜单选中变动
checkedChange (item, checked) {
// 这是是做单选的解决,如果多选去掉此行
this.defaultLabels = []
this.clearTag()
console.log(item, checked)
item.checked = checked
this.$emit('clickItem', item)
this.markChildrenChecked(item)
this.markParentChecked(item)
this.markParentHasCheckChild(item)
this.pickCheckedItem(this.clonedOpts)
this.refresPopover()
this.syncData()},
// 同步数据到下层
syncData () {console.log(this.selectedValues)
this.$emit('input', this.selectedValues)
this.$emit('change', this.selectedValues, this.selectedItems)
},
// 开展下一级
async spreadNext (children, index, item) {
const vm = this
if (
vm.allowLoadChildren &&
!children && !item[vm.childrenKey] &&
vm.loadChildrenMethod &&
vm.loadChildrenMethod.constructor === Function &&
!vm.loadChildrenPromise && // promise 不存在
!item.isLeaf
) {const isPromiseMethod = this.loadChildrenMethod(item)
if (isPromise(isPromiseMethod)) {
vm.loadChildrenPromise = isPromiseMethod
this.$set(item, 'loading', true)
const result = await vm.loadChildrenPromise.catch(e => {this.$set(item, 'loading', false)
})
this.$set(item, 'loading', false)
vm.loadChildrenPromise = null
if (result && result.constructor === Array) {this.recursiveOpt(result, item)
this.$set(item, vm.childrenKey, result)
children = result
this.initDatas()} else {console.warn('The resolved value by loadChildrenMethod must be an Option Array !')
}
} else {console.warn('You must return a Promise instance in loadChildrenMethod !')
}
}
if (index || index === 0) {if (vm.casTree.indexOf(children) === -1) {if (children && children.length > 0) {vm.casTree.splice(index + 1, vm.casTree.length - 1, children)
} else {vm.casTree.splice(index + 1, vm.casTree.length - 1)
}
vm.$emit('spread', item)
}
}
},
visibleChange (visible) {if (visible) {this.showPopover = true}
},
handleFocus (evt) {if (this.disabled) return
this.$emit('focus', evt)
},
hidePopover (evt) {
this.showPopover = false
this.$emit('blur', evt)
},
refresPopover () {setTimeout(() => {fireEvent(window, 'resize')
}, 66)
}
}
}
</script>
<style lang="less">
.hide-popper {display: none;}
.cascader-popper {padding: 0px;}
.crec-popover-inner-content {padding: 0;}
.cascader-menu-wrapper {
white-space: nowrap;
.crec-cascader-menu {
padding: 8px 0;
height: 240px;
}
}
.cascader-menu-wrapper .cascader-checkbox {
margin-right: 10px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
user-select: none;
}
.crec-cascader-menu-item.has-checked-child {background-color: #f5f7fa !important;}
.dropdown__empty {
height: 100%;
padding-top: 50%;
margin: 0;
text-align: center;
color: #999;
font-size: 14px;
}
.can-load-children {position: relative;}
.can-load-children::after {
content: '';
display: inline-block;
position: absolute;
width: 5px;
height: 5px;
background: #a5d279;
right: 20px;
top: 50%;
border-radius: 50%;
transform: translateY(-50%);
-webkit-transform: translateY(-50%);
}
.can-load-children.loading-children::after {
animation: loading 0.22s infinite alternate;
-moz-animation: loading 0.22s infinite alternate; /* Firefox */
-webkit-animation: loading 0.22s infinite alternate; /* Safari 和 Chrome */
-o-animation: loading 0.22s infinite alternate; /* Opera */
}
@keyframes loading {
from {background: #a5d279;}
to {background: #334d19;}
}
</style>
export function deepClone (source) {if (!source && typeof source !== 'object') {throw new Error('error arguments', 'shallowClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {if (source[keys] && typeof source[keys] === 'object') {targetObj[keys] = source[keys].constructor === Array ? [] : {}
targetObj[keys] = deepClone(source[keys])
} else {targetObj[keys] = source[keys]
}
})
return targetObj
}
export function hasArrayChild (obj, childrenKey) {return obj[childrenKey] && Array.isArray(obj[childrenKey])
}
let id = 0
export function getId () {return ++id}
export function fireEvent (element, event) {if (document.createEventObject) {
// IE 浏览器反对 fireEvent 办法
const evt = document.createEventObject()
return element.fireEvent('on' + event, evt)
} else {
// 其余规范浏览器应用 dispatchEvent 办法
const evt = document.createEvent('HTMLEvents')
evt.initEvent(event, true, true)
return !element.dispatchEvent(evt)
}
}
export function isPromise (obj) {return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}
// 所有选项
export const props = {
value: {type: [Array, String],
default () {return ''}
},
placeholder: {
type: String,
default: '请抉择'
},
disabled: {
type: Boolean,
default: false
},
options: {
type: Array,
default () {return []
}
},
defaultOptions: {
type: Array,
default () {return []
}
},
size: {
type: String,
default: ''
},
selectChildren: {
type: Boolean,
default: false
},
noDataText: {
type: String,
default: '无数据'
},
collapseTags: {
type: Boolean,
default: false
},
separator: {
type: String,
default: '/'
},
showAllLevels: {
type: Boolean,
default: false
},
outputLevelValue: {
type: Boolean,
default: false
},
// 显示加载指示器
showLoadingIndicator: {
type: Boolean,
default: true
},
// 容许加载子项
allowLoadChildren: {
type: Boolean,
default: false
},
// 加载办法
loadChildrenMethod: {
type: Function,
default: null,
return: Promise
},
// key
labelKey: {
type: String,
default: 'label'
},
valueKey: {
type: String,
default: 'value'
},
childrenKey: {
type: String,
default: 'children'
},
popperClass: {
type: String,
default: ''
},
clearable: {
type: Boolean,
default: false
},
panelWidth: {type: [Number, String],
default: 160
}
}
import Vue from 'vue'
const isServer = Vue.prototype.$isServer
/* istanbul ignore next */
export const on = (function () {if (!isServer && document.addEventListener) {return function (element, event, handler) {if (element && event && handler) {element.addEventListener(event, handler, false)
}
}
} else {return function (element, event, handler) {if (element && event && handler) {element.attachEvent('on' + event, handler)
}
}
}
})()
const nodeList = []
const ctx = '@@clickoutsideContext'
let startClick
let seed = 0
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e))
!Vue.prototype.$isServer && on(document, 'mouseup', e => {nodeList.forEach(node => node[ctx].documentHandler(e, startClick))
})
function createDocumentHandler (el, binding, vnode) {return function (mouseup = {}, mousedown = {}) {
if (!vnode ||
!vnode.context ||
!mouseup.target ||
!mousedown.target ||
el.contains(mouseup.target) ||
el.contains(mousedown.target) ||
el === mouseup.target ||
(vnode.context.popperElm &&
(vnode.context.popperElm.contains(mouseup.target) ||
vnode.context.popperElm.contains(mousedown.target)))) return
if (binding.expression &&
el[ctx].methodName &&
vnode.context[el[ctx].methodName]) {vnode.context[el[ctx].methodName]()} else {el[ctx].bindingFn && el[ctx].bindingFn()}
}
}
/**
* v-clickoutside
* @desc 点击元素里面才会触发的事件
* @example
* ```vue
* <div v-element-clickoutside="handleClose">
* ```
*/
export default {bind (el, binding, vnode) {nodeList.push(el)
const id = seed++
el[ctx] = {
id,
documentHandler: createDocumentHandler(el, binding, vnode),
methodName: binding.expression,
bindingFn: binding.value
}
},
update (el, binding, vnode) {el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
el[ctx].methodName = binding.expression
el[ctx].bindingFn = binding.value
},
unbind (el) {
const len = nodeList.length
for (let i = 0; i < len; i++) {if (nodeList[i][ctx].id === el[ctx].id) {nodeList.splice(i, 1)
break
}
}
delete el[ctx]
}
}
正文完