Element Plus 组件库实现:3. Tooltip(2)——实现

前言

上篇文章 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函数还需要接受另外一个参数------一个回调函数。
    • 实现:
    ts 复制代码
    import 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组件中并使用:
    ts 复制代码
    import 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组件的优化。如有错误,请大佬评论区批评指正。

相关推荐
undefined&&懒洋洋4 分钟前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者2 小时前
React 19 新特性详解
前端
随云6322 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6322 小时前
WebGL编程指南之进入三维世界
前端·webgl
蜜桃小阿雯2 小时前
JAVA开源项目 旅游管理系统 计算机毕业设计
java·开发语言·jvm·spring cloud·开源·intellij-idea·旅游
寻找09之夏3 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
多多米10054 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱4 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑4 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8564 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序