自定义Vue组件之Tooltip

此文代码大部分借鉴于原创文章:Vue组件之ToolTip

Tooltip

常用于展示鼠标 hover 时的提示信息。

首先让我们来看看,在模版里是如何引用tooltip组件的

<tooltip :placement="'right'" :trigger="'hover'" :width="300">
    <p class="mouse">鼠标放在这里</p>
    <template slot="content">
      <div class="tooltip-content">
        <p>我是tooltip我是tooltip</p>
        <p>我是tooltip我是tooltip</p>
        <p>我是tooltip我是tooltip</p>
      </div>
    </template>
</tooltip>

我们可以看到,类名mouse就是触发提示的dom,而template里头,就是提示的内容部分,这两者都是通过slot插槽完成的,让我们看看组件的模版

<div style="position:relative">
    <span ref="trigger">
      <slot></slot>
    </span>
    <div
      class="tooltip"
      v-bind:class="{'visible':show === true}"
      ref="popover"
      role="tooltip"
    >
      <div class="tooltip-inner">
        <slot name="content" v-html="content"></slot>
      </div>
    </div>
</div>

在ref=trigger下的slot,就是引用组件是存放触发dom的插槽,而下面的<slot name=”content” v-html=”content”></slot>即是存放提示内容的插槽了。
接下来我们看看在javascript代码中,如何设置提示框的显示和隐藏

Tooltip的出现和隐藏

import EventListener from '../utils/EventListener'
export default {
  name: 'Tooltip',
  props: {
    trigger: {
      type: String,
      default: 'hover'
    },
    content: String,
    placement: String,
    width: {
      type: Number,
      default: 200
    }
  },
  data () {
    return {
      show: true
    }
  },
  methods: {
    toggle () {
      this.show = !this.show
    }
  },
  mounted () {
    if (!this.$refs.popover) {
      return console.error('Could not fid popover ref in your component that uses popoverMixin')
    }
    // 获取监听对象
    const triger = this.$refs.trigger.children[0]
    const popover = this.$refs.popover
    // 根据trigger监听特定事件
    if (this.trigger === 'hover') {
      this._mouseenterEvent = EventListener.listen(triger, 'mouseenter', () => {
        this.show = true
      })
      this._mouseleaveEvent = EventListener.listen(triger, 'mouseleave', () => {
       this.show = false
      })
    } else if (this.trigger === 'focus') {
      this._focusEvent = EventListener.listen(triger, 'focus', () => {
        this.show = true;
      })
      this._blurEvent = EventListener.listen(triger, 'blur', () => {
        this.show = false;
      })
    } else {
      this._clickEvent = EventListener.listen(triger, 'click', this.toggle)
    }
    this.show = !this.show
  },
  // 在组件销毁前移除监听,释放内存
  beforeDestroy () {
    if (this._blurEvent) {
      this._blurEvent.remove()
      this._focusEvent.remove()
    }
    if (this._mouseenterEvent) {
      this._mouseenterEvent.remove()
      this._mouseleaveEvent.remove()
    }
    if (this._clickEvent) {
      this._clickEvent.remove()
    }
  }
}

在data中定义了show变量,此变量就是用来控制提示框的显示和隐藏的。为了确保能获取到ref,我们先将其设置为true,在mounted中完成事件绑定后,在将其设置为false,这就完成了初始花了,此时就能触发tooltip了。
当然这里还需要补充css样式

.tooltip {
  visibility: hidden;
  border: 1px solid #aaa;
  background: #fff;
  z-index: 2;
  &.visible {
    visibility: visible;
  }
}

可以看到,这里是根据传入的trigger属性来区分绑定的时间类型的,trigger的默认值是hover,所以如果引用时没有输入的话,就自动绑定hover时间。
注意:在组件销毁前要移除监听,释放内存
到了这里还远远不够,因为tooltip出现是并非是跟随元素的,所以我们接下来要设置tooltip的位置。

设置Tooltip的位置

首先,将tooltip设置为绝对定位

.tooltip {
  position: absolute;
  visibility: hidden;
  border: 1px solid #aaa;
  background: #fff;
  z-index: 2;
  &.visible {
    visibility: visible;
  }
}

然后动态设置定位

export default {
  name: 'Tooltip',
  props: {
    // ...省略
  },
  data () {
    return {
      position: {
        top: 0,
        left: 0
      },
      show: true
    }
  },
  watch: {
    show: function (val) {
      if (val) {
        const popover = this.$refs.popover
        const triger = this.$refs.trigger.children[0]
        switch (this.placement) {
          case 'top':
            this.position.left = triger.offsetLeft - popover.offsetWidth / 2 + triger.offsetWidth / 2
            this.position.top = triger.offsetTop - popover.offsetHeight - 5
            break
          case 'left':
            console.log('width:', popover.offsetWidth)
            this.position.left = triger.offsetLeft - popover.offsetWidth - 5
            this.position.top = triger.offsetTop + triger.offsetHeight / 2 - popover.offsetHeight / 2
            break
          case 'right':
            this.position.left = triger.offsetLeft + triger.offsetWidth + 5
            this.position.top = triger.offsetTop + triger.offsetHeight / 2 - popover.offsetHeight / 2
            break
          case 'bottom':
            this.position.left = triger.offsetLeft - popover.offsetWidth / 2 + triger.offsetWidth / 2
            this.position.top = triger.offsetTop + triger.offsetHeight + 5
            break
        }
        popover.style.top = this.position.top + 'px'
        popover.style.left = this.position.left + 'px'
      }
    }
  },
  methods: {
    // ...省略
  },
  mounted () {
    // ...省略
  },
  // 在组件销毁前移除监听,释放内存
  beforeDestroy () {
    // ...省略
  }
}

给show加上监视,在每次show发生变化时,就重新计算tooltip的位置。
到了这一步,一个最简单的tooltip就已经成形了。
但是这样还不够,你会发现,如果你想操作tooltip里面的元素,比如点击里面的链接,或者为其加上一些事件,但是如果是通过hover来触发的,当鼠标离开后tooltip就马上隐藏了,所以这里需要完善一下————为hover加上延迟关闭

延时隐藏

首先在data中定义一个变量:

showFlag: false

然后,改写一下hover的事件绑定

if (this.trigger === 'hover') {
  this._mouseenterEvent = EventListener.listen(triger, 'mouseenter', () => {
    this.show = true
    this.showFlag = true
  })
  this._mouseleaveEvent = EventListener.listen(triger, 'mouseleave', () => {
    this.showFlag = false
    setTimeout(() => {
      if (!this.showFlag) {
        this.show = false
      }
    }, 1000)
  })
  this._mouseenterEvent1 = EventListener.listen(popover, 'mouseenter', () => {
    this.show = true
    this.showFlag = true
  })
  this._mouseleaveEvent1 = EventListener.listen(popover, 'mouseleave', () => {
    this.showFlag = false
    setTimeout(() => {
      if (!this.showFlag) {
        this.show = false
      }
    }, 1000)
  })
}

当然还要记得在beforeDestroy中销毁

if (this._mouseenterEvent) {
  this._mouseenterEvent.remove()
  this._mouseleaveEvent.remove()
  this._mouseenterEvent1.remove()
  this._mouseenterEvent2.remove()
}

一个遗留的问题

在开发过程中,我发现一个问题,由于使用了绝对定位,导致盒子的宽度变成了自适应宽度。在引用组件时,我将一个句子写在一个p标签中,展示出来的时候却会自动换行,并且定位失准,而且会随着触发次数的增多慢慢边长,最后才完全展开成一行字。描述有点抽象,看图:
《自定义Vue组件之Tooltip》
《自定义Vue组件之Tooltip》
《自定义Vue组件之Tooltip》
《自定义Vue组件之Tooltip》
《自定义Vue组件之Tooltip》
面对这种情况,我没有想出好的办法,只能给开放一个属性,让用户在引用的时候设置它的宽度。

如果小伙伴们对这个Bug有更好的解决方法,欢迎留言!!

所以最后,组件的完整代码如下:

<template>
  <div style="position:relative">
    <span ref="trigger">
      <slot></slot>
    </span>
    <div
      class="tooltip"
      v-bind:class="{
      'visible': show === true
      }"
      ref="popover"
      role="tooltip"
    >
      <div class="tooltip-inner">
        <slot name="content" v-html="content"></slot>
      </div>
    </div>
  </div>
</template>
<script>
import EventListener from '../utils/EventListener'
export default {
  name: 'Tooltip',
  props: {
    trigger: {
      type: String,
      default: 'hover'
    },
    content: String,
    placement: String,
    width: {
      type: Number,
      default: 200
    }
  },
  data () {
    return {
      position: {
        top: 0,
        left: 0
      },
      show: true,
      showFlag: false // 用于判断延迟关闭
    }
  },
  watch: {
    show: function (val) {
      if (val) {
        const popover = this.$refs.popover
        const triger = this.$refs.trigger.children[0]
        switch (this.placement) {
          case 'top':
            this.position.left = triger.offsetLeft - popover.offsetWidth / 2 + triger.offsetWidth / 2
            this.position.top = triger.offsetTop - popover.offsetHeight - 5
            break
          case 'left':
            console.log('width:', popover.offsetWidth)
            this.position.left = triger.offsetLeft - popover.offsetWidth - 5
            this.position.top = triger.offsetTop + triger.offsetHeight / 2 - popover.offsetHeight / 2
            break
          case 'right':
            this.position.left = triger.offsetLeft + triger.offsetWidth + 5
            this.position.top = triger.offsetTop + triger.offsetHeight / 2 - popover.offsetHeight / 2
            break
          case 'bottom':
            this.position.left = triger.offsetLeft - popover.offsetWidth / 2 + triger.offsetWidth / 2
            this.position.top = triger.offsetTop + triger.offsetHeight + 5
            break
        }
        popover.style.top = this.position.top + 'px'
        popover.style.left = this.position.left + 'px'
      }
    }
  },
  methods: {
    toggle () {
      this.show = !this.show
    }
  },
  mounted () {
    if (!this.$refs.popover) {
      return console.error('Could not fid popover ref in your component that uses popoverMixin')
    }
    // 获取监听对象
    const triger = this.$refs.trigger.children[0]
    const popover = this.$refs.popover
    // 根据trigger监听特定事件
    if (this.trigger === 'hover') {
      this._mouseenterEvent = EventListener.listen(triger, 'mouseenter', () => {
        this.show = true
        this.showFlag = true
      })
      this._mouseleaveEvent = EventListener.listen(triger, 'mouseleave', () => {
        this.showFlag = false
        setTimeout(() => {
          if (!this.showFlag) {
            this.show = false
          }
        }, 1000)
      })
      this._mouseenterEvent1 = EventListener.listen(popover, 'mouseenter', () => {
        this.show = true
        this.showFlag = true
      })
      this._mouseleaveEvent1 = EventListener.listen(popover, 'mouseleave', () => {
        this.showFlag = false
        setTimeout(() => {
          if (!this.showFlag) {
            this.show = false
          }
        }, 1000)
      })
    } else if (this.trigger === 'focus') {
      this._focusEvent = EventListener.listen(triger, 'focus', () => {
        this.show = true;
      })
      this._blurEvent = EventListener.listen(triger, 'blur', () => {
        this.show = false;
      })
    } else {
      this._clickEvent = EventListener.listen(triger, 'click', this.toggle)
    }
    this.show = !this.show
  },
  // 在组件销毁前移除监听,释放内存
  beforeDestroy () {
    if (this._blurEvent) {
      this._blurEvent.remove()
      this._focusEvent.remove()
    }
    if (this._mouseenterEvent) {
      this._mouseenterEvent.remove()
      this._mouseleaveEvent.remove()
      this._mouseenterEvent1.remove()
      this._mouseenterEvent2.remove()
    }
    if (this._clickEvent) {
      this._clickEvent.remove()
    }
  }
}
</script>
<style lang="scss">
.tooltip {
  max-width: 500px;
  min-width: 100px;
  position: absolute;
  visibility: hidden;
  border: 1px solid #aaa;
  background: #fff;
  z-index: 2;
  &.visible {
    visibility: visible;
  }
}
</style>

最后奉上EventLisener.js的代码

const EventListener = {
  /**
   * Listen to DOM events during the bubble phase.
   *
   * @param {DOMEventTarget} target DOM element to register listener on.
   * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
   * @param {function} callback Callback function.
   * @return {object} Object with a `remove` method.
   */
  listen (target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove () {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove () {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  }
};

export default EventListener;
    原文作者:pigbb234
    原文地址: https://segmentfault.com/a/1190000020310825
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞