Input输入框的失焦

最近遇到一个需求:需要点击输入框后,出现下拉选项,且下拉选项的内容以树结构展示,如图
在这里插入图片描述
其中遇到一个棘手的问题,在输入框失去焦点时,下拉选项框需要隐藏,
这意味着当我准备点击树节点时,整个下拉选项框就被隐藏了,
所以虽然视觉效果上我点击了节点,但实际上我一直未曾真正点击到。

解决思路
=> 在点击节点时,需要保证输入框不会失去焦点
=> 在下拉选项区域内做操作时,输入框不会失去焦点
=> 输入框的失焦是在鼠标onmousedown事件执行时浏览器的默认行为
=> 给下拉选项区域加上onmousedown事件,并阻止默认行为,代码如下

this.$refs.treeGroup.onmousedown = function (e) {
  if (e && e.preventDefault) {
    // 现代浏览器阻止默认事件
    e.preventDefault()
  } else {
    // IE阻止默认事件
    window.event.returnValue = false
  }
  return false
}

完整代码,仅供学习参考

<!-- 基于 vue.js 2.0 + element-ui 2.14.x -->
<template>
  <div class="select-tree">
    <v-input
      class="search"
      v-model="filterText"
      :placeholder="placeholder"
      :width="width"
      :suffix-icon="showTree ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
      @focus="onFocus"
      @blur="onBlur"
      @clear="clear"
    />
    <div class="tree-group" v-show="showTree" ref="treeGroup">
      <div class="tree-group__box" v-show="treeData.length !== 0">
        <button-tree
          ref="buttonTree"
          class="button-tree"
          :key="key"
          :data="treeData"
          :defaultProps="treeProps"
          :filterText="filterText"
          :highlightCurrent="highlightCurrent"
          :filterNodeMethod="filterNodeMethod"
          :expand-on-click-node="false"
          @nodeClick="nodeClick"
        />
      </div>
      <div v-show="treeData.length === 0" class="tree-group__box is-empty">暂无数据</div>
    </div>
  </div>
</template>

<script>
import { ButtonTree } from 'components/bussiness'

export default {
  name: 'selectTree',
  components: {
    ButtonTree
  },
  props: {
    // 选中项的值,支持`.sync`
    value: {
      type: String,
      require: true
    },
    placeholder: {
      type: String,
      default: '请输入'
    },
    width: {
      type: Number,
      default: 182
    },
    treeData: {
      type: Array,
      default: () => {
        return []
      }
    },
    treeProps: {
      type: Object,
      default: () => {
        return {}
      }
    },
    expandOnClickNode: {
      type: Boolean,
      default: true
    },
    highlightCurrent: {
      type: Boolean,
      default: true
    },
    filterNodeMethod: {
      type: Function
    }
  },
  data () {
    return {
      showTree: false,
      filterText: '',
      result: '',
      model: this.value,
      key: 0
    }
  },
  watch: {
    model (newValue) {
      console.log(newValue, 'newValue')
      // 返回当前所选值的value数组
      this.$emit('update:value', newValue)
    },
    showTree (val) {
      if (val) { return }
      // 树弹框隐藏时触发
      if (this.model === '') {
        this.filterText = ''
      } else {
        this.recursGetData(this.model, this.treeData)
        this.filterText = this.result[this.treeProps.label]
        this.result = null
      }
    }
  },

  mounted () {
    this.$refs.treeGroup.onmousedown = function (e) {
      if (e && e.preventDefault) {
        // 现代浏览器阻止默认事件
        e.preventDefault()
      } else {
        // IE阻止默认事件
        window.event.returnValue = false
      }
      return false
    }
  },
  methods: {
    // input 输入框获取焦点
    onFocus () {
      this.filterText = ''
      setTimeout(() => {
        this.showTree = true
      }, 100)
      this.$emit('focus', event)
    },
    // input 输入框失去焦点
    onBlur () {
      this.showTree = false
      this.$emit('blur', event)
    },
    clear () {
      // this.form.orgId = ''
      this.model = ''
      this.key += 1
      this.$emit('clear')
    },
    nodeClick (data, node, nodeVue) {
      this.filterText = data[this.treeProps.label]
      // this.form.orgId = data[this.treeProps.id]
      this.model = data[this.treeProps.id]
      this.$emit('nodeClick', data, node, nodeVue)
    },

    /* 计算 */
    // 通过值递归获取数据
    recursGetData (id, arr) {
      console.log(id, arr, this.treeProps, 'id, arr')
      arr.map((d, i) => {
        if (id === d[this.treeProps.id]) {
          this.result = d
        } else {
          if (d[this.treeProps.children].length !== 0) {
            this.recursGetData(id, d[this.treeProps.children])
          }
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.select-tree {
  position: relative;
  .search {
    // ::v-deep .el-input__inner {
    //   border-radius:18px;
    //   background: rgba(144,166,186, 0.1);
    //   box-shadow: inset 4px 1px 8px 0px rgba(95,122,155,0.1);
    //   border: none;
    // }
    ::v-deep .el-input__suffix {
      line-height: 32px;
    }
  }
  .tree-group {
    &::before {
      content: '';
      border: 1px solid;
      border-color: #E4E7ED transparent transparent #E4E7ED;
      background-color: #fff;
      width: 6px;
      height: 6px;
      position: absolute;
      top: 2px;
      left: 36px;
      z-index: 4;
      transform: rotateZ(45deg)
    }
    position: absolute;
    left: 0;
    top: 110%;
    z-index: 2;
    .tree-group__box {
      &.is-empty {
        height: auto;
        color: #909399;
        text-align: center;
        padding: 4px 0;
        font-size: 14px;
      }
      /** 不可使用定位,会破坏button-tree组件结构 */
      width: 182px;
      height: 204px;
      border: 1px solid #E4E7ED;
      border-radius: 4px;
      background-color: #FFF;
      box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
      box-sizing: border-box;
      margin: 5px 0;
      padding: 5px 0 0;
    }
  }
}
</style>
// button-tree 组件

<template>
  <div class="tree-button-wrapper" id="treeButtonBox">
    <div class="tree-button-box" id="treeButton">
      <el-tree
        ref="elTree"
        :node-key="nodeKey"
        :default-checked-keys="defaultCheckedKeys"
        :data="data"
        :props="defaultProps"
        :filter-node-method="filterNodeMethod"
        :empty-text="emptyText"
        :lazy="lazy"
        :load="loadData"
        :default-expanded-keys="defaultExpandedKeys"
        :default-expand-all="expandAll"
        :expand-on-click-node="expandOnClickNode"
        :accordion="accordion"
        :highlight-current="highlightCurrent"
        :show-checkbox="showCheckbox"
        :check-on-click-node="checkOnClickNode"
        :draggable="draggable"
        :allow-drag="allowDrag"
        :allow-drop="allowDrop"
        :indent="indent"
        @node-click="nodeClick"
        @node-contextmenu="nodeContextMenu"
        @check-change="checkChange"
        @check="check"
        @current-change="currentChange"
        @node-expand="nodeExpand"
        @node-collapse="nodeCollapse"
        @node-drag-start="nodeDragStart"
        @node-drag-enter="nodeDragEnter"
        @node-drag-leave="nodeDragLeave"
        @node-drag-over="nodeDragOver"
        @node-drag-end="nodeDragEnd"
        @node-drop="nodeDrop">
        <template slot-scope="{ node, data }">
          <div class="custom-tree-node">
            <span class="icon" v-if="data.icon" :style="handleBG(data, node)"></span>
            <div class="node-content" ref="nodeContent" @contextmenu.stop.prevent="contentContextMenu(data, node, $event)">
              <!--传入html-->
              <div style="display: inline-block;" v-if="data && data.html" v-html="data.value"></div>
              <!--传入自定义组件-->
              <component v-else-if="data && data.component" :is="data.componentName" :row="scope.row"></component>
              <!--传入普通文本-->
              <div style="display: inline-block;" v-else v-text="data[defaultProps.label]"></div>
            </div>
            <!-- 按钮列表 -->
            <div class="node-button" v-if="showBtnAll(data)">
              <div class="node-button-icon" @click.stop.prevent="buttonIconClick(data)"><i class="el-icon-more"></i></div>
              <ul class="node-button-list" v-show="showList(data)">
                <li
                  v-show="showBtn(data, item)"
                  v-for="item in btnFilterOps"
                  :key="item.value"
                  @click.stop.prevent="buttonClick(data, item)"
                >
                  {{item.text}}
                </li>
                <!-- <li @click.stop="buttonClick(data, 'add')">添加部门</li> -->
                <!-- <li>添加成员</li>
                <li>编辑</li>
                <li>删除</li>
                <li>移动</li> -->
              </ul>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
  </div>
</template>
<script>
import { Tree } from 'element-ui'
import _ from 'lodash'
// @group 基础组件
// @title Tree 树形组件
export default {
  name: 'button-tree',
  components: {
    'el-tree': Tree
  },
  props: {
    // 是否开启防抖
    isDebounce: {
      type: Boolean,
      default: true
    },
    // 用于显示弹框按钮的列表数据
    btnOps: {
      type: Array,
      default: () => {
        // [{ text: '添加部门', value: 'add' }] text 按钮名称 value 按钮编号
        return []
      }
    },
    // 用于树节点过滤
    filterText: {
      type: String,
      default: ''
    },
    // 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的,建议业务方传入这个属性
    nodeKey: {
      type: String,
      default: 'id'
    },
    // 默认勾选的节点的 key 的数组
    defaultCheckedKeys: {
      type: Array
    },
    // 与data配套使用,详情请参考element-ui的tree组件的props介绍:http://element-cn.eleme.io/#/zh-CN/component/tree
    defaultProps: {
      type: Object,
      default: () => {
        return {
          // 必传,唯一性id
          id: 'id',
          // 必传,label 指定节点标签为节点对象的某个属性值 string, function(data, node)
          label: 'label',
          // 必传,children 指定子树为节点对象的某个属性值 string
          children: 'children',
          // disabled 指定节点选择框是否禁用为节点对象的某个属性值 boolean, function(data, node)
          disabled: 'disabled',
          // isLeaf 指定节点是否为叶子节点,仅在指定了 lazy 属性的情况下生效 boolean, function(data, node)
          isLeaf: 'isLeaf',
          // btn 指定节点存在哪些功能按钮,Array<>
          btn: 'btn'
        }
      }
    },
    // 展示数据
    data: {
      type: Array,
      default: () => {
        return []
      }
    },
    // 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏
    filterNodeMethod: {
      type: Function
    },
    // 内容为空的时候展示的文本
    emptyText: {
      type: String,
      default: '暂无数据'
    },
    // 是否懒加载子节点,需与 load 方法结合使用
    lazy: {
      type: Boolean,
      default: false
    },
    // 默认展开的节点的 key 的数组
    defaultExpandedKeys: {
      type: Array
    },
    // 是否默认展开所有节点
    expandAll: {
      type: Boolean,
      default: false
    },
    // 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
    expandOnClickNode: {
      type: Boolean,
      default: true
    },
    // 是否每次只打开一个同级树节点展开
    accordion: {
      type: Boolean,
      default: false
    },
    // 是否高亮当前选中节点,默认值是 false。
    highlightCurrent: {
      type: Boolean,
      default: false
    },
    // 节点是否可被选择
    showCheckbox: {
      type: Boolean,
      default: false
    },
    // 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。
    checkOnClickNode: {
      type: Boolean,
      default: false
    },
    // 是否开启拖拽节点功能
    draggable: {
      type: Boolean,
      default: false
    },
    // 判断节点能否被拖拽
    allowDrag: {
      type: Function
    },
    // 拖拽时判定目标节点能否被放置。type 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后
    allowDrop: {
      type: Function
    },
    // 相邻级节点间的水平缩进,单位为像素
    indent: {
      type: Number,
      default: 16
    },
    // 自定义树节点的图标
    iconClass: {
      type: String,
      default: 'el-icon-plus'
    }
  },
  data () {
    return {
      // 选中节点的数据,此处选中为点击显示按钮的图标,不要修改该数据
      currentData: {},
      scrollAction: {
        x: undefined,
        y: undefined
      },
      // 当前展开的节点的vnode对象,再 loadData 方法中被使用
      currentNode: null
    }
  },
  computed: {
    btnFilterOps () {
      return this.btnOps.filter(d => {
        // return this.$store.getters.getPermission(d.value)
        /** 测试用代码 */
        return true
        /** end */
      })
    }
  },
  watch: {
    data: {
      handler () {
        // 初始化一次div.node-button元素的位置
        this.$nextTick(() => {
          this.nodeButtonElementMove()
        })
      },
      deep: true,
      immediate: true
    },
    currentNode: {
      handler (val) {
        if (val && val.loading === false) {
          this.$nextTick(() => {
            this.nodeButtonElementMove(val)
          })
          this.currentNode = null
        }
      },
      deep: true
    },
    filterText (val) {
      if (this.isDebounce) {
        this.filter(val)
      } else {
        this.$refs.elTree.filter(val)
      }
    }
  },
  mounted () {
    // 操作dom必须再节点挂载之后,为treeButtonBox元素绑定滚动事件
    this.treeButtonBoxElementBindEvent()
    // 为过滤事件绑定this值
    // this.filter.bind(this)
  },
  methods: {
    // 不可以使用箭头函数,this指向会出现问题
    filter: _.debounce(function (val) {
      console.log('过滤节点触发')
      this.$refs.elTree.filter(val)
    }, 100),
    // 是否显示所有与弹出按钮相关的内容
    showBtnAll (data) {
      return this.btnFilterOps.length !== 0 && data[this.defaultProps.btn] && data[this.defaultProps.btn].length !== 0
    },
    // 是否显示按钮列表
    showList (data) {
      const currentId = this.currentData[this.defaultProps.id]
      const dataId = data[this.defaultProps.id]
      // console.log(currentId, dataId, currentId === dataId)
      return currentId !== undefined && dataId === currentId
    },
    // 是否显示按钮
    showBtn (data, item) {
      // const permission = this.$store.getters.getPermission(item.value)
      // return this.defaultProps.btn && data[this.defaultProps.btn].includes(item.value) && permission
      console.log('是否显示按钮')
      /** 测试用代码 */
      return data[this.defaultProps.btn].includes(item.value)
      /** end */
    },
    // 按钮图标被点击时触发
    buttonIconClick (data) {
      const currentId = this.currentData[this.defaultProps.id]
      const dataId = data[this.defaultProps.id]
      // 若点击节点是当前存储节点,则清除存储节点数据
      if (currentId !== undefined && dataId === currentId) {
        this.currentData = {}
      } else {
        this.currentData = data
      }
    },
    // 为treeButtonBox元素绑定滚动事件
    treeButtonBoxElementBindEvent () {
      const elTreeBox = document.getElementById('treeButtonBox')
      elTreeBox.addEventListener('scroll', () => {
        if (this.scrollAction.x !== undefined && this.scrollAction.x !== elTreeBox.scrollLeft) {
          this.nodeButtonElementMove()
        }
      })
    },
    // 为div.node-button元素设置left值
    nodeButtonElementMove (data) {
      // 若按钮无需显示,避免运行下面的代码消耗性能
      if (!this.btnFilterOps.length) {
        return
      }
      if (data && data[this.defaultProps.btn] && data[this.defaultProps.btn].length === 0) {
        return
      }
      const el = window.document.getElementById('treeButtonBox')
      this.scrollAction.x = el.scrollLeft
      el.querySelectorAll('.node-button').forEach(e => {
        // 注意: 这里的21px是div.node-button元素的高度,因为旋转了90deg,高成了宽
        e.style.left = `${el.scrollLeft + el.clientWidth - 22}px`
      })
    },
    // 按钮被点击时触发
    // @arg (data, value)接收两个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
    // @arg 2.item 按钮对应的对象
    buttonClick (data, item) {
      this.currentData = {}
      this.$emit('buttonClick', data, item)
    },
    loadData (node, resolve) {
      // 加载子树数据的方法,仅当 lazy 属性为true 时生效
      // @arg (node, resolve) 接收两个参数,1. 当前节点 2. 渲染数据的方法
      // console.log(node, resolve, 'reslove')
      this.currentNode = node
      this.$emit('loadData', node, resolve)
    },
    // @vuese
    // 自定义树节点图标的方法
    handleBG (data, node) {
      let expanded = node.expanded
      let bgUrl
      if (!expanded) {
        // 未打开
        bgUrl = `url(${data.icon.iconOpen})`
      } else {
        // 已打开
        bgUrl = `url(${data.icon.iconClose})`
      }
      return {
        backgroundImage: bgUrl
      }
    },
    // @vuese
    // 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组
    // @arg (leafOnly, includeHalfChecked)接收两个参数 1.leafOnly 是否只是叶子节点,默认值为 false
    // @arg 2.includeHalfChecked 是否包含半选节点,默认值为 false
    getCheckedNodes (leafOnly, includeHalfChecked) {
      return this.$refs.elTree.getCheckedNodes(leafOnly, includeHalfChecked)
    },
    // @vuese
    // 设置目前勾选的节点,使用此方法必须设置 node-key 属性
    // @arg (nodes)接收一个参数 1.nodes 接收勾选节点数据的数组
    setCheckedNodes (nodes) {
      this.$refs.elTree.setCheckedNodes(nodes)
    },
    // @vuese
    // 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组
    // @arg (leafOnly)接收一个参数 1.leafOnly 若为 true 则仅返回被选中的叶子节点的 keys,默认值为 false
    getCheckedKeys (leafOnly) {
      return this.$refs.elTree.getCheckedKeys(leafOnly)
    },
    // @vuese
    // 通过 keys 设置目前勾选的节点,使用此方法必须设置 node-key 属性
    // @arg (keys, leafOnly)接收两个参数 1.keys 勾选节点的 key 的数组
    // @arg 2.leafOnly 若为 true 则仅设置叶子节点的选中状态,默认值为 false
    setCheckedKeys (keys, leafOnly) {
      this.$refs.elTree.setCheckedKeys(keys, leafOnly)
    },
    // @vuese
    // 获取当前被选中节点的 key,使用此方法必须设置 node-key 属性,若没有节点被选中则返回 null
    getCurrentKey () {
      return this.$refs.elTree.getCurrentKey()
    },
    // @vuese
    // 获取当前被选中节点的 data,若没有节点被选中则返回 null
    getCurrentNode () {
      return this.$refs.elTree.getCurrentNode()
    },
    // @vuese
    // 通过 key 设置某个节点的当前选中状态,使用此方法必须设置 node-key 属性
    // @arg (key)接收一个参数 1.key 待被选节点的 key,若为 null 则取消当前高亮的节点
    setCurrentKey (key) {
      this.$refs.elTree.setCurrentKey(key)
    },
    // @vuese
    // 通过 node 设置某个节点的当前选中状态,使用此方法必须设置 node-key 属性
    // @arg (node)接收一个参数 1.node 待被选节点的 node
    setCurrentNode (node) {
      this.$refs.elTree.setCurrentNode(node)
    },
    // @vuese
    // 根据 data 或者 key 拿到 Tree 组件中的 node
    // @arg (data)接收一个参数 1.data 要获得 node 的 key 或者 data
    getNode (data) {
      return this.$refs.elTree.getNode(data)
    },
    // @vuese
    // 删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
    // @arg (data)接收一个参数 1.data 要删除的节点的 data 或者 node
    remove (data) {
      this.$refs.elTree.remove(data)
    },
    // @vuese
    // 为 Tree 中的一个节点追加一个子节点
    // @arg (data, parentNode)接收两个参数 1.data 要增加的节点的 data
    // @arg 2.parentNode 要增加的节点的后一个节点的 data、key 或者 node
    append (data, parentNode) {
      this.$refs.elTree.append(data, parentNode)
    },
    // @vuese
    // 为 Tree 的一个节点的前面增加一个节点
    // @arg (data, refNode)接收两个参数 1.data 要增加的节点的 data
    // @arg 2.refNode 要增加的节点的后一个节点的 data、key 或者 node
    insertBefore (data, refNode) {
      this.$refs.elTree.insertBefore(data, refNode)
    },
    // @vuese
    // 为 Tree 的一个节点的后面增加一个节点
    // @arg (data, refNode)接收两个参数 1.data 要增加的节点的 data
    // @arg 2.refNode 要增加的节点的前一个节点的 data、key 或者 node
    insertAfter (data, refNode) {
      this.$refs.elTree.insertAfter(data, refNode)
    },
    nodeClick (data, node, nodeVue) {
      // 节点被点击时的回调
      // @arg (data, node, nodeVue)接收三个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 2.node 节点对应的 Node
      // @arg 3.nodeVue 节点组件本身
      console.log('节点被点击')
      // 节点被点击后
      this.$emit('nodeClick', data, node, nodeVue)
    },
    nodeContextMenu (event, data, node, nodeVue) {
      // 当某一节点被鼠标右键点击时会触发该事件
      // @arg (event, data, node, nodeVue)接收四个参数 1.event 事件对象
      // @arg 2.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 3.node 节点对应的 Node
      // @arg 4.nodeVue 节点组件本身
      this.$emit('nodeContextMenu', event, data, node, nodeVue)
    },
    contentContextMenu (data, node, event) {
      // 当某一节点内容被鼠标右键点击时会触发该事件
      // @arg (event, data, node)接收三个参数 1.event 事件对象
      // @arg 2.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 3.node 节点对应的 Node
      this.$emit('contentContextMenu', event, data, node)
    },
    checkChange (data, checked, childrenChecked) {
      // 节点选中状态发生变化时的回调
      // @arg (data, checked, childrenChecked)接收三个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 2.checked 节点本身是否被选中
      // @arg 3.childrenChecked 节点的子树中是否有被选中的节点
      this.$emit('checkChange', data, checked, childrenChecked)
    },
    check (data, checkedObj) {
      // 当复选框被点击的时候触发
      // @arg (data, checkedObj)接收两个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 2.checkedObj 树目前的选中状态对象,包含 checkedNodes、checkedKeys、halfCheckedNodes、halfCheckedKeys 四个属性
      this.$emit('check', data, checkedObj)
    },
    currentChange (data, node) {
      // 当前选中节点变化时触发的事件
      // @arg (data, node)接收两个参数 1.data 当前节点的数据
      // @arg 2.node 当前节点的 Node 对象
      this.$emit('currentChange', data, node)
    },
    nodeExpand (data, node, nodeVue) {
      // 节点被展开时触发的事件
      // @arg (data, node, nodeVue)接收三个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 2.node 节点对应的 Node
      // @arg 3.nodeVue 节点组件本身
      // 当并非懒加载时,节点展开后,且dom挂载完成时,需要将div.node-button元素位置重新计算一次
      if (!this.lazy) {
        this.$nextTick(() => {
          this.nodeButtonElementMove()
        })
      }
      this.$emit('nodeExpand', data, node, nodeVue)
    },
    nodeCollapse (data, node, nodeVue) {
      // 节点被关闭时触发的事件
      // @arg (data, node, nodeVue)接收三个参数 1.data 传递给 data 属性的数组中该节点所对应的对象
      // @arg 2.node 节点对应的 Node
      // @arg 3.nodeVue 节点组件本身
      this.$emit('nodeCollapse', data, node, nodeVue)
    },
    nodeDragStart (node, event) {
      // 节点开始拖拽时触发的事件
      // @arg (node, event)接收两个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.event 事件对象
      this.$emit('nodeCollapse', node, event)
    },
    nodeDragEnter (node, enterNode, event) {
      // 拖拽进入其他节点时触发的事件
      // @arg (node, enterNode, event)接收三个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.enterNode 所进入节点对应的 Node
      // @arg 3.event 事件对象
      this.$emit('nodeDragEnter', node, enterNode, event)
    },
    nodeDragLeave (node, leaveNode, event) {
      // 拖拽离开某个节点时触发的事件
      // @arg (node, leaveNode, event)接收三个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.leaveNode 所离开节点对应的 Node
      // @arg 3.event 事件对象
      this.$emit('nodeDragLeave', node, leaveNode, event)
    },
    nodeDragOver (node, enterNode, event) {
      // 在拖拽节点时触发的事件(类似浏览器的 mouseover 事件)
      // @arg (node, enterNode, event)接收三个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.enterNode 当前进入节点对应的 Node
      // @arg 3.event 事件对象
      this.$emit('nodeDragOver', node, enterNode, event)
    },
    nodeDragEnd (node, overNode, pos, event) {
      // 拖拽结束时(可能未成功)触发的事件
      // @arg (node, overNode, pos, event)接收四个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.overNode 结束拖拽时最后进入的节点(可能为空)
      // @arg 3.pos 被拖拽节点的放置位置(before、after、inner)
      // @arg 4.event 事件对象
      this.$emit('nodeDragEnd', node, overNode, pos, event)
    },
    nodeDrop (node, overNode, pos, event) {
      // 拖拽成功完成时触发的事件
      // @arg (node, overNode, pos, event)接收四个参数 1.node 被拖拽节点对应的 Node
      // @arg 2.overNode 结束拖拽时最后进入的节点
      // @arg 3.pos 被拖拽节点的放置位置(before、after、inner)
      // @arg 4.event 事件对象
      this.$emit('nodeDrop', node, overNode, pos, event)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
 .tree-button-wrapper {
  position: relative; // 该属性不可删除
  height: 100%;
  overflow: auto;
  .tree-button-box {
    min-width: 100%;
    display: inline-block;
    // ::v-deep .el-tree{
    // }
  }
  // .el-tree-node>.el-tree-node__children {
  //   overflow: auto;
  // }
  ::v-deep .el-tree-node__content {
    height: 24px;
    position: relative;
    &:hover{
      z-index: 3;
      .custom-tree-node {
        .node-content {
          opacity: 0.3;
        }
        .node-button {
          .node-button-icon {
            display: inline-block;
          }
        }
      }
    }
    .custom-tree-node {
      display: inline-block;
      height: 24px;
      line-height: 24px;
      .icon {
        margin: 0 4px 0 0;
        width: 22px;
        height: 22px;
        display: inline-block;
        vertical-align: top;
        // vertical-align: ;
        border: 0 none;
        cursor: pointer;
        outline: none;
        background-repeat: no-repeat;
        background-attachment: scroll;
        background-size: contain;
      }
      .node-content {
        display: inline-block;
        padding: 0;
      }
      .node-button {
        // display: none;
        position: absolute;
        top: 0;
        right: 8px;
        width: 18px;
        height: 100%;
        font-size: 20px;
        color: #909399;
        text-align: center;
        .node-button-icon {
          display: none;
          transform: rotateZ(-90deg);
        }
        .node-button-list {
          user-select: none;
          // display: none;
          position: absolute;
          top: 24px;
          right: 8px;
          list-style: none;
          font-size: 14px;
          color: #909399;
          padding: 2px 0px;
          border: 1px solid #e4e7ed;
          border-radius: 4px;
          background-color: #fff;
          box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
          z-index: 1;
          > li {
            padding: 0px 12px;
            &:hover {
              color: #1B8CF2;
              background-color: #f5f7fa;
            }
            // text-decoration: underline;
            // list-style: none;
          }
        }
      }
    }
  }
  ::v-deep .el-tree-node__children {
    overflow: inherit;
  }
}
</style>
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Vue3中,如果点击输入框以外的地方没有触发失焦事件,而只有再次点击并聚焦输入框,再点击输入框以外的地方,失焦事件才能触发,这可能是因为在模板中使用了条件渲染生成的输入框,并没有通过聚焦事件focus触发。 要解决这个问题,你可以在输入框上添加ref属性,并通过该ref属性在Vue组件中获取输入框的DOM元素。例如,在模板中给输入框添加ref属性:`<input class="ring-2" ref="nameInput" v-else v-model="name" type="text" @blur="submitEditName" @keyup.enter="submitEditName" />` 然后,在Vue组件的方法中,可以通过`this.$refs.nameInput`来获取到输入框的DOM元素,并在需要的时候进行操作。比如,你可以在失焦事件blur中调用提交编辑的方法submitEditName,代码如下: ```javascript methods: { submitEditName() { // 处理提交编辑的逻辑 } } ``` 这样,无论是点击输入框以外的地方还是按下回车键,都会触发失焦事件,从而调用提交编辑的方法。这样就可以实现输入框失焦的功能了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【Vue实践】Vue中Input失焦事件无效问题解决](https://blog.csdn.net/baidu_36511315/article/details/118516072)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值