vue自定义指令实现弹出框位置变化监听

2,302 阅读3分钟

前言

之前在公司使用iview-admin的popTip组件的时候遇到一个问题,弹出框会被overflow:hidden遮挡。因为是置于元素内部的。之后看到iview给了transfer的配置,把弹出框置于body内,而且元素位置发生变化时(譬如滚动或者窗口缩放)弹出框依然可以动态改变topleft来紧贴元素。我就想自己也跟着试试实现。

为什么用自定义指令

因为我觉得在组件里面,弹出框的出现是非常频繁的,需求也部分相同,例如气泡框,确认框。封装一个指令可以减轻以后代码负担。

初始代码

<template>
  <div ref="popover">
    <div ref="reference" v-show="visible">我是弹出框内容</div>
    <span ref="trigger">
      <button>开启弹出框</button>
    </span>
  </div>
</template>

弹出框位置设置

弹出框设为绝对定位,位于body的第一层级。

```
  updateReference() {
         const {reference, trigger} = this.$refs;
         document.body.appendChild(contentWrapper)
            let {top, left, height, width} = trigger.getBoundingClientRect()
            //根据top和left设置弹出框的位置
            this.$nextTick(() => {
                //设置transform来调整弹出框位于元素的位置,像是左边或顶部
            }))
        },
```

为何要用nextTick: 因为绝对定位下,弹出框的宽度可能会随着left的变化而变化(除非定死宽度),这里是为了准确的获取到设置left后弹出框的宽度

获取所有可以滚动的父元素

以下代码参考于Popper.js

 const isBody = (node)=>{
    let name =node.nodeName.toLowerCase()
    return name === 'body'||name==='html' //因为之前钩子里如果父节点不存在会出现死循环,加了一个html
}
 const getParentNode = (el) => {
    if (getNodeName(el) === 'html') return el
    return (
        el.parentNode || el.host || document.ownerDocument || document.documentElement
    )
}

const isHTMLElement = node=> node instanceof window.HTMLElement

 const getAllScrollParents = (node,list)=> {
    if(!list)list = []
    if (isBody(node))return list
    if (isHTMLElement(node)) {
        const {overflow, overflowX, overflowY} = getComputedStyle(node)
        if (/auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX)) {
            list.push(node)
        }
    }
    return getAllScrollParents(getParentNode(node),list)
    //这里做一个元素收集,后面直接遍历监听
}

自定义指令实现监听

先简单介绍一下即将用到的自定义指令的钩子函数参数

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)

事件监听绑定就在这个钩子里面进行,因为这时候元素的父元素一定是存在的,如果父元素不存在,那么之前的getParentNode就会获取到错误的父节点了,这也是为什么不用bind钩子的原因。

unbind:只调用一次,指令与元素解绑时调用。

移除事件监听就是在unbind进行了。

binding参数对象

  • value:指令绑定值,这里即为传入的updateReference函数。
  • expression:字符串形式的指令表达式,这里就是updateReference,也就是函数的名字
//detective.js
   inserted(el, binding){
        function handler(e) {
    
            if (binding.expression) binding.value(e)
        }
        let $parents =  getAllScrollParents(el)
        $parents.forEach(node => {
            if (node) node.addEventListener('scroll', handler)//遍历监听
        })
        window.addEventListener('resize', handler)//监听窗口变化
        el._clickHandler = handler //利用el做方法传递,以便与unbind钩子里可以获取方法
        el._$scrollParents = $parents
    },

至于为什么用el传递数据

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

 unbind(el) {
        if(!el._$scrollParents)el._$scrollParents = getAllScrollParents(el)
        el._$scrollParents.forEach(node => {
            if (node) node.removeEventListener('scroll',el._clickHandler)
        })
        window.removeEventListener('resize',el._clickHandler)
        delete el._clickHandler
        delete el._$scrollParents

    }

最后的组件代码

<template>
  <div v-element-position-detector="updateReference" ref="popover">
    <div ref="reference" v-show="visible">我是弹出框内容</div>
    <span ref="trigger">
      <button>开启弹出框</button>
    </span>
  </div>
</template>
import elementPositionDetector from "../detective.js"
export default {
     directives:{elementPositionDetector},
    
}

效果

popover组件

简易代码示例

如果你打开控制台,尝试滚动或者缩放窗口就会发现弹出层的位置变化。最后iviewiview-admin是真的香。