前言
上篇文章 Element Plus 组件库实现:3. Tooltip(1) - 掘金 (juejin.cn) 介绍了实现一个Tooltip组件的大概思路及方案,本文将详细介绍Tooltip的具体实现。
Tooltip组件
html
<script setup lang="ts">
import { ref, watch, reactive, onMounted, computed } from 'vue';
import { createPopper } from '@popperjs/core'
import type { TooltipProps, TooltipEmits, TooltipInstance } from './types'
import type { Instance } from '@popperjs/core'
defineOptions({
name: 'YvTooltip'
})
// props属性
const props = withDefaults(defineProps<TooltipProps>(), {
placement: 'bottom',
// 默认是hover触发
trigger: 'hover',
})
// 整个Tooltip组件节点,在处理点击空白处关闭提示功能中,需要用到这个节点
const popperContainNode = ref<HTMLElement>()
// 触发区域节点
const triggerNode = ref<HTMLElement>()
// 展示区域节点,结合popper使用
const popperNode = ref<HTMLElement>()
// popper实例
let popperInstance: null | Instance = null
// 事件派发
const emits = defineEmits<TooltipEmits>()
<template>
<!-- 最外层节点绑定一个outerEvents -->
<div class="yv-tooltip" ref="popperContainNode" v-on="outerEvents">
<!-- 触发区域 -->
<!-- 触发区域也绑定一个events-->
<!-- outerEvents和events将在下文中介绍 -->
<div class="yv-tooltip__trigger" v-on="events" ref="triggerNode">
<slot></slot>
</div>
<!-- 展示区域 -->
<div v-if="isOpen" class="yv-tooltip__popper" ref="popperNode">
<slot name="content">{{ content }}</slot>
<div id="arrow" data-popper-arrow></div>
</div>
</div>
</template>
结合上文设计思路:
- 实现基本的显示/隐藏:
ts
const isOpen = ref<boolean>(false)
// 打开提示
const tooltipOpen = () => {
isOpen.value = true
// 派发事件
emits('visible-change', true)
}
// 关闭提示
const tooltipClose = () => {
isOpen.value = false
emits('visible-change', false)
}
- 实现click/hover触发:
ts
// 用一个events来保存click/hover对应的触发,并且绑定在触发区域对应的节点上
let events: Record<string, any> = reactive({})
// outerEvents绑定在最外层节点上
let outerEvents: Record<string, any> = reactive({})
// 根据传进来的trigger属性来确定是click还是hover触发
const attachEvents = () => {
if (props.trigger === 'hover') {
events['mouseenter'] = tooltipOpen
outerEvents['mouseleave'] = tooltipClose
} else {
events['click'] = handlePopper
}
}
// 这样做的前提是没有设置手动触发
if (!props.manual) {
attachEvents()
}
可能你已经注意到了,上边还有一个outerEvents,而且它是绑定在最外层节点中,看起来它的作用也是用来保存事件,那么他的作用是什么呢,主要是用于处理hover出发时可能出现的错误,来看下面这张图:
所以需要在父节点上绑定处理hover触发时对应的事件,
outerEvents['mouseleave'] = tooltipClose
即表示,鼠标离开整个Tooltip区域才关闭提示
-
支持点击空白处隐藏:
- 分析: 支持点击空白处隐藏,其实也就是点击Tooptip组件之外的区域进行隐藏,那么这个时候可以使用一个hook函数来进行处理,这个hook函数有什么功能呢?首先需要接受一个DOM节点,然后,监听整个页面的点击事件。当展示区域处于打开的状态时,如果发生了点击事件,判断点击是否发生在传入的这个DOM节点区域,如果不在,那么就执行相应的回调函数,使展示区域关闭,那么也就是这个hook函数还需要接受另外一个参数------一个回调函数。
- 实现:
tsimport type { Ref } from 'vue' import { onMounted, onUnmounted } from 'vue' // hook函数,接受两个参数 const useCilckOutside = ( element: Ref<undefined | HTMLElement>, callback: (e: MouseEvent) => void ) => { const handleClick = (e: MouseEvent) => { // e.target即表示点击事件发生的元素 if (element.value && e.target) { // 判断Tooltip组件是否包含被点击的事件, // 如果不包含,说明点击的是Tooltip组件外部,这个时候就可以实现关闭了 // 注意这里需要一个类型断言,因为DOM事件对象的target属性通常被推断为EventTarget类型, // 但是这里是一个HTMLElement类型 if (!element.value?.contains(e.target as HTMLElement)) { callback(e) } } } // 然后监听页面点击事件 onMounted(() => { document.addEventListener('click', handleClick) }) // 页面销毁,取消监听 onUnmounted(() => { document.removeEventListener('click', handleClick) }) } export default useCilckOutside
- 然后将这个hook函数引入Tooltip组件中并使用:
tsimport useCilckOutside from '@/hooks/useUtilTooltip' useCilckOutside(popperContainNode, () => { if (props.trigger && isOpen.value && !props.manual) { closeTooltip() } if (isOpen.value) { emits('click-outside', true) } })
-
支持手动触发:
ts
// 监听是否手动
watch(() => props.manual, (isManual) => {
if (isManual) {
events = {}
outerEvents = {}
} else {
attachEvents()
}
})
- 支持popper参数:
那么就需要拓展prop属性:
ts
import type { Placement, Options } from '@popperjs/core'
export interface TooltipProps {
// 显示内容
content?: string
// 触发方式
trigger?: 'hover' | 'click'
// 显示方式
placement?: Placement
manual?: boolean
// 丰富popper配置项
popperOptions?: Partial<Options>
}
ts
// Tooltip.vue
// popper属性,各配置作用不再做介绍,详见官方文档
const popperOptions = computed(() => {
return {
placement: props.placement,
modifiers: [
{
name: 'offset',
options: {
offset: [0, 9],
},
}
],
...props.popperOptions
}
})
实现各种效果
ts
// 监听弹框是否关闭
watch(isOpen, (newVal) => {
if (newVal) {
if (triggerNode.value && popperNode.value) {
// 打开状态,创建popperInstance实例
popperInstance = createPopper(triggerNode.value, popperNode.value, popperOptions.value)
} else {
// 关闭状态,销毁popperInstance实例
popperInstance?.destroy()
}
}
// 注意这里需要在DOM更新完成之后进行监听
}, { flush: 'post' })
// 监听触发方式
watch(() => props.trigger, (newVal, oldVal) => {
if (newVal !== oldVal) {
events = {}
outerEvents = {}
attachEvents()
}
})
// 监听是否手动
watch(() => props.manual, (isManual) => {
if (isManual) {
// 手动模式需要清空click/hover对应的事件
events = {}
outerEvents = {}
} else {
attachEvents()
}
})
onMounted(() => {
popperInstance?.destroy()
})
// 将打开和关闭方法暴露出去,在手动出触发时使用
defineExpose<TooltipInstance>({
'show': tooltipOpen,
'hide': tooltipClose
})
总结
以上内容根据设计思路进行Tooltip组件的基本实现,但仅仅是基本实现,还有诸多细节没有考虑,如防抖优化等必备的功能,之后将详细介绍Tooltip组件的优化。如有错误,请大佬评论区批评指正。