此文代码大部分借鉴于原创文章: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标签中,展示出来的时候却会自动换行,并且定位失准,而且会随着触发次数的增多慢慢边长,最后才完全展开成一行字。描述有点抽象,看图:
面对这种情况,我没有想出好的办法,只能给开放一个属性,让用户在引用的时候设置它的宽度。
如果小伙伴们对这个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;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。