目录引言制定结构创建组件文件,实现基本功能绝对定位如何点击外部关闭Bug:监听body问题。Bug:再次打开失败。Bug:点击popover气泡本身也会关闭popover其他BugB
最近做了一套Vue 的UI组件框架,里面牵涉到的popover组件个人觉得很有意思,也是个人觉得做出来最好看的一个组件。
首先新建一个Vue项目,无需赘述了。
给组件命名为bl-popover
<bl-popover>
<template slot="content">
这是内容,这是内容,这是内容。
这是内容,这是内容,这是内容。
</template>
<button>点击,显示内容</button>
</bl-popover>
这种结构也许不错。
content
的slot
包裹popover里需要显示的内容,而原始默认slot
里包裹popover触发器。
src
目录创建popover.vue
文件。
<template>
<div class="popover">
<slotname="content"></slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
}
</script>
<style lang="sCSS" scoped>
.popover{
display:inline-block
}
</style>
文件内部结构先写成这样,符合我对使用结构的印象,接着想要测试的话就注册这个组件,也无需赘述了。
设置为display:inline-block
可不用占满一整行。
我们需要用触发器来显示和隐藏popover,所以在data里设置一个show
属性。
让触发器被点击实现切换。但由于slot
标签是不能接受任何东西的,所以我们把事件绑定在整个div上。
就变成了
<template>
<div class="popover" @click="showChange">
<slotname="content" v-if="show"></slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
}
}
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block
}
</style>
此时即可实现点击button
就可显示popover。
这时候我们需要做的就是将popover变为绝对定位。
给slot
标签外包裹标签即可选中slot。
<template>
<div class="popover" @click="showChange">
<div class="content-wrapper" v-if="show">
<slotname="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
}
}
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block;
position: relative;
.content-wrapper{
position:absolute;
bottom:100%;
left:0;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
}
}
</style>
即可点击之后显示成这样
name我该如何关闭这个popover呢?是点其他地方关闭吗?
本想这样处理:
methods:{
showChange(){
this.show = !this.show
if(this.show===true){
document.body.addEventListener('click',()=>{
this.show = false
})
}
}
}
但事实是,这样连popover都无法打开了。这是由于原生js的事件冒泡机制。 this.show = !this.show
和 this.show = false
是在一次点击下全部完成了,所以他就直接给关了,根本看不见。
故这里我们将他改为异步,就不会一口气都走完了。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
setTimeout(()=>{
document.body.addEventListener('click', () => {
this.show = false
console.log('关闭show');
})
})
}
}
}
}
即可解决这个开了就关的问题。
但是还有其他的问题。 实际上,body的大小只有蓝色边框内的部分
也就是点击蓝色边框之外的部分,是关不掉这个popover的。
所以不要监听body,直接监听document就好。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
setTimeout(()=>{
document.addEventListener('click', () => {
this.show = false
console.log('关闭show');
})
})
}
}
}
}
解决了点击外部失效的问题,我发现,点击打开popover,再点击外部关闭,就无法再次打开popover了。
这里来看控制台。
第一次点击触发器
第二次点击外部
第三次点击触发器
会发现第三次点击直接走完了切换和关闭。
这是为什么呢,因为这时候有两个事件监听器在运作,一个是popover上的,一个是document上的。顺序是先调用popover上的,再调用document上的。
我们再来看看第四次点击触发器
再看看第五次,第六次
会发现关闭show出现越来越多次,这是为什么呢。这是因为我们点击一次触发器,执行一次showChange
方法,就会在document上新增一个addEventListener
,而我们并没有在时间结束之后删除他,就越来越多,越来越多。
那么我们就需要在每次popover关闭之后,删除他。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
setTimeout(()=>{
document.addEventListener('click', function listener{
this.show = false
console.log('关闭show');
document.removeEventListener('click',listener)
console.log('删除监听器');
}.bind(this))
})
}
}
}
}
这里我们需要removeEventListener
,所以监听器需要有个函数名,我起名为listener
,但不是箭头函数了,this.show
的this就不是指向Vue实例了,而是调用这个监听器的document了。所以需要使用bind
把this绑定一下。
此时前两次点击是
但是第三次点击
说明还是有bug,看起来这个删除监听器根本就没有成功。
这里的原因比较复杂。我为了让listener内的this还是指向Vue实例,使用了bind
,但其实使用了bind
之后的listener并不是原本的listener了,而是绑定后返回的一个新的函数。所以并没有删掉原本的listener。所以在这里要避免使用bind
。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
setTimeout(()=>{
let listener = () =>{
console.log('新增事件监听器');
this.show = false
console.log('关闭show');
document.removeEventListener('click',listener)
console.log('删除监听器');
}
document.addEventListener('click',listener)
})
}
}
}
新建了一个箭头函数,就避免了使用bind
。
这是此时点击了六次的结果,可以正常开闭popover了。
虽然开闭正常了,但是点击气泡本身,我本身不希望他隐藏,可他还是关闭了。
这是因为事件冒泡的原因,我们点击popover或者触发器,事件会冒泡到document上面去,还是会触发。
我这时选择了这个处理方法
<template>
<div class="popover" @click.stop="showChange">
<div class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
this.$nextTick(()=>{
let listener = () =>{
console.log('新增事件监听器');
this.show = false
console.log('关闭show');
document.removeEventListener('click',listener)
console.log('删除监听器');
}
document.addEventListener('click',listener)
})
}
}
}
}
</script>
在可以被点击的地方使用了.stop
阻止冒泡,可以发现,点击气泡不会被关闭了,并且document上的事件监听器也没有产生或触发。
这样就实现了一个最简单的popover。
我在popover组件的外部套一个div,设置overflow:hidden,会发生这样的情况
会被挡住。
说明这个问题非常严重,代码可能要全部砍倒重练。
而且单纯地阻止冒泡也会带来很多问题,会打断用户的事件链。
那我选择让这个弹出框气泡移到body上,就可以避免这个问题。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-show="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
this.$nextTick(()=>{
let listener = () =>{
console.log('新增事件监听器');
this.show = false
console.log('关闭show');
document.removeEventListener('click',listener)
console.log('删除监听器');
}
document.addEventListener('click',listener)
})
}
}
},
mounted() {
document.body.appendChild(this.$refs.contentWrapper)
}
}
</script>
可以看到,为了能让v-if===false
的情况下,也能检查的到contentWrapper,我把v-if
换成了v-show
,因为v-show
只是切换display:none
,影响的是元素的显示隐藏。而v-if
影响的是元素是否被render到DOM树上。
但是使用v-show
就会让contentWrapper一开始就存在在页面上,我并不想这样。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切换show');
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let listener = () =>{
console.log('新增事件监听器');
this.show = false
console.log('关闭show');
document.removeEventListener('click',listener)
console.log('删除监听器');
}
document.addEventListener('click',listener)
})
}
}
},
}
</script>
这样,让我点击触发器的时候,再将弹出框移动到body上,也可以。记住这一步要放在nextTick
里,不然还是可能找不到contentWrapper。
这时候我要想办法让这个弹出框像以前一样显示。首先要找到触发器的位置。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top,width,height} = this.$refs.triggerWrapper.getBoundinGClientRect()
console.log(left, top, width, height);
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},
这样就可以让contentWrapper在一个正确的位置了。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top +'px'
this.$refs.contentWrapper.style.left = left + 'px'
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block;
position: relative;
}
.content-wrapper{
position:absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transfORM: translateY(-100%);
}
</style>
注意要把.content-wrapper移出来,因为.content-wrapper已经不在.popover里了,而我们还在使用scoped。
我们在整个页面上端再加一个div试试
会发现位置根本不对。
这是因为,绝对定位并未根据触发器为基准,而是根据他的父元素body元素为基准的。
而body顶部到视窗的差值,就是scrollY
横向也一样的道理。 所以改成
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top +scrollY +'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},
就正常了。
<template>
<div class="popover" @click="showChange">
<div v-if="show" ref="contentWrapper" class="content-wrapper">
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data() {
return {
show: false
}
},
methods: {
position() {
document.body.appendChild(this.$refs.contentWrapper)
let {left, top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top + scrollY + 'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
},
eventListener() {
let listener = (event) => {
if (!this.$refs.contentWrapper.contains(event.target)) {
this.show = false
document.removeEventListener('click', listener)
}
}
document.addEventListener('click', listener)
},
showChange(event) {
if (this.$refs.triggerWrapper.contains(event.target)) {
this.show = !this.show
console.log('打开');
if (this.show === true) {
this.$nextTick(() => {
this.position()
this.eventListener()
})
}
}
}
},
}
</script>
<style lang="scss" scoped>
.popover {
display: inline-block;
position: relative;
}
.content-wrapper {
position: absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transform: translateY(-100%);
}
</style>
让我们来判断你点击的是什么,然后再决定要做什么,就可以避免这个问题了。
只点击触发器,会重复监听,而且不会实行removeEventListener。
那我们把document.addEventListener
放在created里是不是会方便很多呢?
确实会方便很多,但是,如果我页面上有100个这个组件,那我打开页面就要加100个监听器,那完蛋了。所以不可以这样。
这个时候我要进行的是一个高内聚的设计模式。
将close和open抽离出来,大家都用这两个控制开闭。
<template>
<div ref="popover" class="popover" @click="showChange">
<div v-if="show" ref="contentWrapper" class="content-wrapper">
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data() {
return {
show: false
}
},
methods: {
position() {
document.body.appendChild(this.$refs.contentWrapper)
let {left, top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top + scrollY + 'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
},
listener(event) {
if (this.$refs.popover &&
this.$refs.popover.contains(event.target) || this.$refs.popover === event.target) {
return;
}
if (this.$refs.contentWrapper &&
this.$refs.contentWrapper.contains(event.target) || this.$refs.contentWrapper === event.target) {
return;
}
this.close()
},
close() {
this.show = false
document.removeEventListener('click', this.listener)
},
open() {
this.show = true
this.$nextTick(() => {
this.position()
document.addEventListener('click', this.listener)
})
},
showChange(event) {
if (this.$refs.triggerWrapper.contains(event.target)) {
if (this.show === true) {
this.close()
} else {
this.open()
}
}
}
},
}
</script>
<style lang="scss" scoped>
.popover {
display: inline-block;
position: relative;
}
.content-wrapper {
position: absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transform: translateY(-100%);
}
</style>
这样高聚合了之后,将close
和open
两个方法聚合了所有和开闭弹出层有关的东西。
这样就完成了一个基础的,可点击在上方出现的popover。
剩下的,Hover触发,四个方向触发,具体样式,也是依葫芦画瓢,这里就不多赘述了。
以上就是Vue基础popover弹出框编写及bug问题分析的详细内容,更多关于Vue popover弹出框的资料请关注编程网其它相关文章!
--结束END--
本文标题: Vue基础popover弹出框编写及bug问题分析
本文链接: https://lsjlt.com/news/167005.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0