Vue造轮子系列-Popover组件

4,051 阅读3分钟

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结构部分还是挺简单的,这里主要是有两部分组成,

  1. 第一部分class='popover-wrapper'div就表示popover的提示框,然后里面的<slot name='content' />表示提示框里面的内容,然后下面的div是确定、取消按钮,只有当用户传了confirm参数时才会出现;
  2. 第二部分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触发方式stringclick / hoverclick
position出现位置stringtop / bottom / left / righttop
confirm是否显示确认按钮booleanfalse

接下去思考以下几个问题,

  1. 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?
  2. 提示框怎么出现在按钮的上方呢?
1、 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?

先解释第一个问题,在解决这个问题的时候,我是这么思考的

  1. 我一开始是直接给body添加click事件,然后判断popover是否显示,把它的值取反就好了。但是这个办法不行,因为当你点击按钮的时候,body绑定的点击事件也执行了,所以提示出不来,

  2. 想到异步绑定,所以在this.$nextTick()中去绑定,这时候看似可以解决了问题,但是又会有一个新的问题,就是你每点击一次就会绑定一个事件,所以body上就会出现很多点击事件,

  3. 然后呢,就想着那我在每次点击之后,就把绑定的事件给remove掉,那样就不会出现很多的事件了,这个问题虽然解决了,

  4. 然后又出来一个问题,因为我绑定的事件是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

  1. 然后触发器点击,会切换提示框是否显示,并且会给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、 提示框怎么出现在按钮的上方呢?

然后解释一下第二个问题,就是怎么把提示框放到按钮上方去?

  1. 需要把提示框添加到body上去,而不是直接在原来的元素下面,因为这样子可以确保不受overflow:hidden的影响

  2. 那么就来看一下怎么把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'
},
  1. 这段代码主要的意思,就是获取到popoverWrapper,然后把它添加到body上去,接着获取triggerWrapper的宽高,topleft值,然后设置popoverWrappertopleft,定位到按钮上方去,上面的topbottomleftright分别表示出现在上、下、左、右四个方位,然后这里的pageYOffsetpageXOffset意思是如果,屏幕出现滚动条,那么要把超出去的部分加上去,因为triggerWrapper获取到的topleft是相对于视口的。
3、click和hover事件的处理

由于支持clickhover两种方式触发,那么在处理显示提示框的时候也会有区别,这里直接看下代码:

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组件-遇到的问题

碰到的问题在上面实现过程中也已经介绍过了,主要就是下面两个问题,并且两个问题都解决了很久,踩了很多的坑,可以详见我上面的实现过程。

  1. 当提示框出现的时候,当我点击其它地方怎么使它隐藏呢?
  2. 提示框怎么出现在按钮的上方呢?

5、结束

到这里,popover组件就已经完成了,可以看出来,这个组件比前几个组件难了很多,虽然从使用上来说,好像挺简单的,但是自己实现的过程中,还是有很多难点的。所以还是要写啊,不能看着简单就不实现了,好了,这个组件就介绍完了,加油~

项目地址Yue UI