前言
之前在公司使用iview-admin的popTip组件的时候遇到一个问题,弹出框会被overflow:hidden遮挡。因为是置于元素内部的。之后看到iview给了transfer的配置,把弹出框置于body内,而且元素位置发生变化时(譬如滚动或者窗口缩放)弹出框依然可以动态改变top和left来紧贴元素。我就想自己也跟着试试实现。
为什么用自定义指令
因为我觉得在组件里面,弹出框的出现是非常频繁的,需求也部分相同,例如气泡框,确认框。封装一个指令可以减轻以后代码负担。
初始代码
<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进行了。
- 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},
}
效果
如果你打开控制台,尝试滚动或者缩放窗口就会发现弹出层的位置变化。最后iview和iview-admin是真的香。