<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 = 0export 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 startClicklet 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] }}