坑点1:挂载位置
el-popover是弹出来就追加到全局的body里面的,并且关闭弹窗后不会销毁而是隐藏,在弹出框非常多的情况下,会给body下挂载非常多的dom元素。像这样子。

坑点2:v-model
给el-popover加上v-model属性时,所有依赖这个data的弹窗el-popover将会瞬间追加到body尾部,比如你有一个表格,表格里面每一行都有一个元素点击后有弹出框,如果恰好你用一个data来绑定v-model属性,所有行的弹出框都会瞬间追加到body底部,所以如果要使用v-model属性的话必须使用给每一个弹出框单独绑定一个data,并且这个id是随机生成的,不可控。在弹出框多的情况下非常难维护控制弹出框显示隐藏的数据结构,并且如果你对弹出框做了某些监听任务的话,会产生非常多的观察者类。
我的做法是只保留最新的el-popover,其它缓存的DOM全部销毁。因为我的业务只需要对当前的弹出框进行操作。
js
let popDoms = Array.from(document.body.children).filter(function (el) {
return el.id && el.id.indexOf('el-popover-') === 0
})
// el-popover创建弹出框是一直追加隐藏,不销毁的。
let popDom
if (popDoms.length > 1) {
popDom = popDoms[1]
popDoms[0].parentNode.removeChild(popDoms[0])
} else {
popDom = popDoms[0]
}
坑点3:位置
el-popover的位置会跟随它最近的一个有滚动条的容器,但是如果容器外面还有滚动条,el-popover的位置将不会跟随最外层容器的滚动条,导致位置错乱。
如下图,在弹出框弹出的状态下,滚动内部的滚动条,弹出框位置会跟随,但是滚动外部的滚动条,内部的容器位置变了,但是弹出框的位置不会变。导致位置错乱。
我的做法是监听最外层容器的滚动事件,将弹出框的top属性减去最外层容器的scrollTop(滚动条离滚动轨道顶部的距离)的差值。这个差值是结束滚动时候的scrollTop减去开始滚动时候的scrollTop,因为如果外部滚动条已经滚动了一段距离后,弹出框再弹出,这个时候再滚动外部滚动条,如果单纯减去scrollTop的话,是不会符合预期的。
js
scrollWarpDom.addEventListener(
// 弹出框跟随大滚动条滚动
'scroll',
() => {
popDom.style.top =
this.popDomOffsetTop -
(scrollWarpDom.scrollTop - this.scrollWarpDomScrollTop) +
'px'
}
)
// this.scrollWarpDomScrollTop 是当前滚动条的位置,每一次触发弹窗时需要记录初始外部滚动条的位置。
// this.popDomOffsetTop 是当前弹出框的offsetTop,在挂载在body的情况下等于style.top,每一次滚动内部滚动条的时候需要记录一下弹出框的位置。
坑点4:层级
由于el-popover是追加到body后面的,层级是在body下面的那一位,而不是和当前触发弹窗的元素一个层级,所以如果超出了当前元素容器的范围,不会自动消失,而是永远处于顶层的位置。
如下图,如果当前绑定弹出框的元素在表格里面,表格发生滚动,弹出框会跟随表格里的绑定元素移动,到溢出容器的部分,表格滚动容器的内容会被遮挡,但是由于弹出框是挂载在body下面的,层级非常高,所以弹出框是可视的,不符合预期。
我的做法是监听滚动容器的滚动事件,当超出容器范围的时候,先把当前弹出框元素display设置为none,然后再随便触发一个无关紧要的点击事件(el-popover可以通过ref来使用实例的doClose()方法来关闭弹窗,在控制台的确看到实例上有这个方法,但是我实际试了一下不管用,因此采取了这个方式来触发el-popover原始的关闭事件,因为这种情况下el-popover组件是个黑盒,我不确定是否还有其他配置需要修改,所以最稳妥的方式还是让逻辑在组件本身闭环)。如果单纯只隐藏元素的话,下一次需要点击两次才能触发弹窗,因为还欠了一次关闭的点击事件。
js
// 表格滚动,要刷新当前基准位置
let scrollTable = document.querySelector(
'.el-table__body-wrapper'
)
scrollTable.addEventListener('scroll', () => {
// 刷新基准位置,因为滑动了内部的滚动条,之后,弹出框位置会发生变化
this.popDomOffsetTop = parseInt(popDom.style.top)
this.scrollWarpDomScrollTop = parseInt(scrollWarpDom.scrollTop)
// 最外部滚动条滚动的距离+弹出框距顶部的距离+弹出框本身的高度,等于弹出框底部在页面上的位置
let popBottomPos = scrollWarpDom.scrollTop + popDom.offsetTop + popDom.clientHeight
// 超出了表格区域,弹出框消失。
let scrollTableRect = scrollTable.getBoundingClientRect()
if (
(popBottomPos <
scrollTableRect.top + this.scrollWarpDomScrollTop ||
popBottomPos >
scrollTableRect.bottom + this.scrollWarpDomScrollTop) &&
popBottomPos !== scrollWarpDom.scrollTop // 这里是异步原因,当popDom刚生成的时候element有个生成动画,相关属性会为0
) {
popDom.style.display = 'none' // 直接关闭避免抖动
scrollWarpDom.click() // 触发点击事件让el-popoer再关闭一次
}
// 框离顶部或者底部很近的话直接消失,因为这个系统的最大容器是#app, 弹出框和app是同级的,会把body撑大,然后会又出现一个滚动
if (
parseInt(popDom.style.top) < 20 ||
parseInt(popDom.style.top) + popDom.clientHeight > 900
) {
popDom.style.display = 'none' // 直接关闭避免抖动
scrollWarpDom.click() // 触发点击事件让el-popoer再关闭一次
}
})