Vue造轮子系列-Popover组件
1、前言
今天更新的是Popover组件,就是平时常用的气泡提示框。这个组件,看起来很简单,无非就是鼠标移到按钮上面或者点击按钮的时候,出现一个提示框,给这个提示框定位一下就可以了,在没有做之前,大家应该都是这么一个认知,我自己在实现之前也是这么认为的,然而,真正实现的时候,发现并没有那么的容易。下面就直接开始吧!
2、Popover组件-需求
这个组件的需求还是明确的,也比较简单,一个就是点击按钮或者移入按钮,会在按钮上、下、左、右中一个方向出现提示框,然后按钮也可以换成文字等其他你想触发的内容。
再一个就是,提示框里面你可以增加确定,取消按钮,比如下面这样:
上面两点呢就是我在造这个组件时考虑的,那么下面就来看看如何实现吧。
3、Popover组件-实现
还是以前的套路,明确需求之后,当然是先看下,作为用户怎么去用我们的组件,
<y-popover trigger="hover">
<template slot="content">
<div>Hello World</div>
</template>
<y-button>点我</y-button>
</y-popover>
知道怎么调用之后,就可以直接开始撸代码了,先来看下我是怎么写的,然后后面我慢慢解释我实现的思路。
<template>
<div class="y-popover" ref="popover">
<div
ref="popoverWrapper"
class="popover-wrapper"
:class="{ [`position-${position}`]: true }"
v-if="visible"
>
<y-icon v-if="confirm" name="error"></y-icon>
<slot name="content"></slot>
<div v-if="confirm" style="margin-top: 10px; float: right;">
<y-button size="small" type="text" @click="close">取消</y-button>
<y-button size="small" type="primary" @click="close">确定</y-button>
</div>
</div>
<span
ref="triggerWrapper"
class="trigger-wrapper"
style="display: inline-block;"
>
<slot></slot>
</span>
</div>
</template>
整体的html结构部分还是挺简单的,这里主要是有两部分组成,
- 第一部分
class='popover-wrapper'的div就表示popover的提示框,然后里面的<slot name='content' />表示提示框里面的内容,然后下面的div是确定、取消按钮,只有当用户传了confirm参数时才会出现; - 第二部分
class='trigger-wrapper'的span就表示触发器,然后里面的<slot name='content' />表示触发器,比如按钮或者文字等等。
知道了组件的构成之后,然后就是组件会接收哪些参数,看下面的代码,
props: {
confirm: {
type: Boolean,
default: false,
},
position: {
type: String,
default: 'top',
validator(value) {
return ['top', 'bottom', 'left', 'right'].indexOf(value) >= 0
},
},
trigger: {
type: String,
default: 'click',
validator(value) {
return ['click', 'hover'].indexOf(value) >= 0
},
},
},
上面的三个参数解释,看下面这个表格
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| trigger | 触发方式 | string | click / hover | click |
| position | 出现位置 | string | top / bottom / left / right | top |
| confirm | 是否显示确认按钮 | boolean | — | false |
接下去思考以下几个问题,
- 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?
- 提示框怎么出现在按钮的上方呢?
1、 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?
先解释第一个问题,在解决这个问题的时候,我是这么思考的
-
我一开始是直接给
body添加click事件,然后判断popover是否显示,把它的值取反就好了。但是这个办法不行,因为当你点击按钮的时候,body绑定的点击事件也执行了,所以提示出不来, -
想到异步绑定,所以在
this.$nextTick()中去绑定,这时候看似可以解决了问题,但是又会有一个新的问题,就是你每点击一次就会绑定一个事件,所以body上就会出现很多点击事件, -
然后呢,就想着那我在每次点击之后,就把绑定的事件给
remove掉,那样就不会出现很多的事件了,这个问题虽然解决了, -
然后又出来一个问题,因为我绑定的事件是
body上面的,然后我点击提示框,它也给我隐藏掉了,这明显是不行的,这让用户怎么复制提示框的内容,所以再想办法呗,然后我想到了,每次点击的时候,根据e.target去判断,是不是在我的提示框里面,如果是就不隐藏,如果不是就隐藏,来看下代码
eventHandler(e) {
if (this.$refs.popover && (this.$refs.popover === e.target || this.$refs.popover.contains(e.target))) {
return
}
if (this.$refs.popoverWrapper && (this.$refs.popoverWrapper === e.target || this.$refs.popoverWrapper.contains(e.target))) {
return
}
this.close()
},
上面代码就是判断我点击的是不是在popover里面,如果在里面就直接return,只有在其他的地方才会去执行close。
- 然后触发器点击,会切换提示框是否显示,并且会给document添加
click事件,代码如下:
handleClick(event) {
if (this.$refs.triggerWrapper.contains(event.target)) {
if (this.visible === true) {
this.close()
} else {
this.onShow()
}
}
},
onShow() {
this.visible = true
this.$nextTick(() => {
this.positionContent()
document.addEventListener('click', this.eventHandler)
})
},
close() {
this.visible = false
document.removeEventListener('click', this.eventHandler)
},
这里,会在show的时候给document添加click事件,然后在close的时候去移除click事件。
2、 提示框怎么出现在按钮的上方呢?
然后解释一下第二个问题,就是怎么把提示框放到按钮上方去?
-
需要把提示框添加到
body上去,而不是直接在原来的元素下面,因为这样子可以确保不受overflow:hidden的影响 -
那么就来看一下怎么把
popover给移到body上去,并且要定位到按钮上方去,
positionContent() {
const { triggerWrapper, popoverWrapper } = this.$refs
document.body.appendChild(popoverWrapper)
let { width, height, top, left } = triggerWrapper.getBoundingClientRect()
let { height: height2 } = popoverWrapper.getBoundingClientRect()
let positions = {
top: {
top: top + window.pageYOffset,
left: left + window.pageXOffset,
},
bottom: {
top: top + height + window.pageYOffset,
left: left + window.pageXOffset,
},
left: {
top: top + (height - height2) / 2 + window.pageYOffset,
left: left + window.pageXOffset,
},
right: {
top: top + (height - height2) / 2 + window.pageYOffset,
left: left + width + window.pageXOffset,
},
}
popoverWrapper.style.left = positions[this.position].left + 'px'
popoverWrapper.style.top = positions[this.position].top + 'px'
},
- 这段代码主要的意思,就是获取到
popoverWrapper,然后把它添加到body上去,接着获取triggerWrapper的宽高,top、left值,然后设置popoverWrapper的top,left,定位到按钮上方去,上面的top、bottom、left、right分别表示出现在上、下、左、右四个方位,然后这里的pageYOffset和pageXOffset意思是如果,屏幕出现滚动条,那么要把超出去的部分加上去,因为triggerWrapper获取到的top、left是相对于视口的。
3、click和hover事件的处理
由于支持click和hover两种方式触发,那么在处理显示提示框的时候也会有区别,这里直接看下代码:
mounted() {
if (this.trigger === 'click') {
this.$refs.popover.addEventListener('click', this.handleClick)
} else {
this.$refs.popover.addEventListener('mouseenter', this.onShow)
this.$refs.popover.addEventListener('mouseleave', this.close)
}
},
beforeDestroy() {
if (this.trigger === 'click') {
this.$refs.popover.removeEventListener('click', this.handleClick)
} else {
this.$refs.popover.removeEventListener('mouseenter', this.onShow)
this.$refs.popover.removeEventListener('mouseleave', this.close)
}
},
上面的代码应该很容易理解,就是在mounted时候绑定事件,然后beforeDestroy的时候移除事件。
4、Popover组件-遇到的问题
碰到的问题在上面实现过程中也已经介绍过了,主要就是下面两个问题,并且两个问题都解决了很久,踩了很多的坑,可以详见我上面的实现过程。
- 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?
- 提示框怎么出现在按钮的上方呢?
5、结束
到这里,popover组件就已经完成了,可以看出来,这个组件比前几个组件难了很多,虽然从使用上来说,好像挺简单的,但是自己实现的过程中,还是有很多难点的。所以还是要写啊,不能看着简单就不实现了,好了,这个组件就介绍完了,加油~
项目地址Yue UI