关于前端:多选联动标签

56次阅读

共计 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]
  }
}

正文完
 0