美文网首页前端攻城狮让前端飞Web前端技术分享
vue自定义指令实现点击空白区域收起下拉窗口

vue自定义指令实现点击空白区域收起下拉窗口

作者: jingqian_xi | 来源:发表于2018-03-01 23:49 被阅读419次

    (一)场景:
    例如百度的搜索框,当我们输入搜索内容的时候会弹出搜索建议提示窗,然后当我们点击其他区域的时候这个弹窗就会收起。上一张图看一下:


    demo.png

    (二)先要知道的基础点:

    1. vm.$isServer

    当前 Vue 实例是否运行于服务器。

    该属性不是直接定义在实例对象 vm 上,而是定义在原型对象上 => Vue.prototype.$isServer
    该属性根据当前运行的环境以及 process.ven.VUE_ENV 自动设置。

    1. node.contains( otherNode )
      如果 otherNode 是 node 的后代节点或是 node 节点本身.则返回true , 否则返回 false.

    2. vue的自定义指令 官方文档

    示例写法:

    import Vue from 'vue'
    Vue.directive('demo', {
      // 当被绑定的元素插入到dom时……
      inserted (el) {},
      // 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
      bind (el, binding, vnode) {},
      // 所在组件的 VNode 更新时调用
      update (el, binding, vnode) {},
      // 只调用一次,指令与元素解绑时调用
      unbind (el, binding, vnode) {}
    })
    

    使用:

    <div v-demo>
    

    bind,update,unbind等等这些都是钩子函数,它们会被传入一些参数(下面说明的都是要用到的,了解更多请移步官方文档):

    • el:指令所绑定的元素,可以用来直接操作 DOM
    • binding
      • name:指令名
      • value:绑定的值
      • expression:指令表达式
    • vnode:虚拟节点

    (三)实现:
    以下代码参考自elementui的源码,会有解读

    import Vue from 'vue'
    
    const ctx = 'clickoutside'
    const nodeList = []
    const isServer = Vue.prototype.$isServer
    let seed = 0
    let startClick
    
    // 用来绑定事件的方法,它是一个自执行的函数,会根据是否运行于服务器和是否支持addEventListener来返回一个function
    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)
          }
        }
      }
    })()
    // 返回一个方法用来在点击的时候触发函数(触发之前会判断该元素是不是el,是不是focusElment以及他们的后代元素,如果是则不会执行函数)
    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.focusElment &&
            (vnode.context.focusElment.contains(mouseup.target) ||
            vnode.context.focusElment.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()
        }
      }
    }
    
    if (!isServer) {
      on(document, 'mousedown', e => (startClick = e))
      on(document, 'mouseup', e => {
        // 循环所有的绑定节点,把它们的documentHandler属性所绑定的函数执行一次(这个时候得到的刚好是上面的那个判断执行的函数)
        nodeList.forEach(node => node[ctx].documentHandler(e, startClick))
      })
    }
    
    Vue.directive('clickoutside', {
      // 当被绑定的元素插入到dom时……
      inserted (el) {
        console.log(el)
      },
      // 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
      // 把绑定的元素扔到nodeList里面,并给绑定元素设置属性
      // documentHandler属性在nodeList.forEach的时候执行并得到一个function
      // bindingFn 是绑定的那个值,用来执行的
      bind (el, binding, vnode) {
        nodeList.push(el)
        const id = seed++
        el[ctx] = {
          id,
          documentHandler: createDocumentHandler(el, binding, vnode),
          methodName: binding.expression,
          bindingFn: binding.value
        }
      },
      // 所在组件的 VNode 更新时调用
      update (el, binding, vnode) {
        el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
        el[ctx].methodName = binding.expression
        el[ctx].bindingFn = binding.value
      },
      // 只调用一次,指令与元素解绑时调用
      unbind (el, binding, vnode) {
        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]
      }
    })
    

    使用:

    <template lang='pug'>
      .test-wrap
        input.input(
          v-model='text'
          v-clickoutside='handleClose'
          @focus='visible = true'
          placeholder='请填写搜索内容'
        )
        ul.list-wrap(v-show='visible' ref='listWrap')
          li(v-for='item in list' @click='setValue(item.text)') {{item.text}}
    </template>
    <script>
    export default {
      data () {
        return {
          text: '',
          visible: false,
          list: [{
            text: '1111',
            id: 1
          }, {
            text: 'aaaaa',
            id: 2
          }]
        }
      },
      computed: {
        focusElment () {
          return this.$refs.listWrap
        }
      },
      methods: {
        handleClose () {
          this.visible = false
        },
        setValue (val) {
          this.text = val
        }
      }
    }
    </script>
    

    调用解析:

    1. template里面设置v-clickoutside='handleClose'调用这个自定义指令
    2. list-wrap用visible来控制显示
    3. 把list-wrap设置为focusElment,目的是在点击这个区域的时候不去触发bindingFn,源码中(vnode.context.focusElment &&
      (vnode.context.focusElment.contains(mouseup.target) ||
      vnode.context.focusElment.contains(mousedown.target))这段代码解决这个问题
    4. handleClose函数设置visible的值为false,以隐藏下拉窗口

    这个实现就分享这些,欢迎拍砖~

    相关文章

      网友评论

      • 疾风劲草ccy:ready() {
        document.addEventListener('click', (e) => {
        if (!this.$el.contains(e.target)) this.show = false
        })
        }
        0bfce20a708e:你好 你的这条代码是放在vue里哪个位置呢

      本文标题:vue自定义指令实现点击空白区域收起下拉窗口

      本文链接:https://www.haomeiwen.com/subject/iwgnxftx.html