kits组件构建之tooltip

341 阅读9分钟

前言

这篇文章来讲一讲我在kits-tooltip组件构建过程中遇到的一些问题,可以分为一下几点

  1. 结构问题
  2. 位置与方向问题
  3. 边界问题

结构问题

因为要做的是一个组件库中的组件,所以使用的方式无非是采用与其他组件库相似的包裹式使用法

    <k-tooltip content="我是span-上边提示1" position="top"> 
        <k-button>我是span1</k-button> 
    </k-tooltip> 

但是考虑到解析后需要维持原有结构的层级,因此并不希望 k-tooltip 以dom的形式出现在浏览器中导致k-button外层多出一层不必要的dom结构,我们希望的是在浏览中展示的仅有被包裹的button,鼠标与button交互依旧能够保证tooltip的工作正常,如下图: 根据以上设想,那么k-tooltip组件的结构大致就是如下仅有一个slot构成的组件结构:

<!--k-tooltip.vue--> 
<template> 
  <slot></slot> 
</template> 

上面的结构可以满足对该组件的设想,但是也因此产生了一个新的问题: 如何从slot中获取dom来进行功能的后续编写?

一时间被这个问题绊住,毫无头绪,还一度想采用k-tooltip生成dom结构的方案来处理 峰回路转,在我不停的查找答案的过程中,终于找到了解决办法: $slots + render

官方api是这么介绍的:

image.png 也就是说通过slots 可以获取到slot中的vnode相关信息,那么再想办法将其渲染为真实dom即可获取到想要操作的dom

说到渲染首先想到的就是render函数了 在vue3的setup语法糖写法中,想要使用$slots,需要如下方式

import { useSlots } from 'vue'; 
const slots = useSlots(); 
// 获取默认插槽中的vnode信息 
const defaultSlot = slots.default && slots.default()[0]; 

为了方便后续使用,新建一个ts文件来编写render相关逻辑

// createSlot.ts import { defineComponent } from 'vue'; 
export default ({ mountedCallFun }) => { 
  return defineComponent({ 
    props: ['vnode'], 
    mounted() { 
      mountedCallFun(this.$el); 
    }, 
    render(props: any) { 
      return props.vnode; 
    }, 
  }); 
}; 

在k-tooltip组件中进行使用

<template> 
  <kSlot :vnode="defaultSlot"></kSlot> 
</template> 

import createSlot from './createSlots'; 
const kSlot = createSlot({ mountedCallFun }); 
// mountedCallFun为kSlot组件的mounted中的回调函数,可以通过该回调获取到dom元素 
const mountedCallFun = (args) => { 
  // args是即为我们需要的dom 
  dom.value = args; 
};

到这一步,第一个问题已经解决了,下一步开始考虑提示框的位置与方向

位置与方向问题

在获取到目标dom后,所需要思考的问题是提示框如何根据目标dom的位置显示 这又引出了新的问题:

  1. 目标dom的位置信息获取(相对于视口的信息)
  2. 提示框相对于目标dom的位置计算
  3. 小三角的位置

目标dom的位置信息

目标dom的位置信息是相对好获取的,在这里推荐一个api: getBoundingClientRect() getBoundingClientRect() 返回的是矩形的集合,表示了当前盒子在浏览器中的位置以及自身占据的空间的大小,除了 width 和 height 以外的属性是相对于 视图窗口的左上角 来计算的.因此使用该api获取相对视口的信息最为合适

提示框的位置

提示框位置要根据目标dom的位置来进行计算,实际上只需要目标dom的left, top, width和 height即可计算出提示框的位置.

以上边的提示框的top计算为例:

由此可以得出提示框的top信息,以此类推可以算出提示框的left信息,这时提示框的位置信息就确定了

其他方向的提示框也是同样的分析方式得出计算公式来进行计算

小三角的位置

小三角的位置不同可表示tooltip方向不同,在当前组件中仅构思了上下左右四个方向

小三角采用伪元素的boder来构建,这样做的好处,在下一个问题中进行详细解答

小三角的位置采用了css来进行处理,不同的位置使用k-tooltip传入的position属性设置不同的类名来达到效果

边界问题

在开发过程中,tooltip的实现相对来说是比较简单的,但是需要考虑的边界条件也不少:

  1. 窗口缩放时提示框跟随目标dom
  2. 页面滚动时提示框的处理
  3. 提示框的渲染与删除
  4. 鼠标从目标dom移动到提示框上,保持提示框不消失
  5. 在屏幕边界时,提示框的宽高位置重载

窗口缩放时提示框跟随目标dom

在窗口缩放时因为改变了目标dom的相对位置,因此会出现问题,由于使用了相对位置进行计算,所以该问题已经得到了解决

页面滚动时提示框的处理

在页面中有滚动条时,提示框会根据滚动的高度去改变自身top的数,使显示发生偏移

这个问题是由于定位的处理不当,在这里需要将提示框的定位修改为固定定位,即可解决问题

提示框的渲染与删除

提示框的渲染与删除是一个比较大的问题,这里需要思考的是:

  1. 何时渲染
  2. 渲染的方式
  3. 何时删除

何时渲染很好解决,在对目标dom进行交互时渲染比如 click hover时 渲染的方式: 采用createElement的方式创建提示框通过append添加到body中

这里需要考虑一个问题: 页面上有多个k-tooltip时是如何处理,通过v-if控制还是从始至终仅有一个提示框,但在实际页面中同时不可能出现多个tooltip进行展示,因此提示框的渲染采用从始至终仅有一个提示框的方式,那么就与下个问题产生了关联-->何时删除

何时删除这个问题,根据上述可以得出一个每次交互之后将提示框进行删除(后面会推翻,离谱) 在代码层面实现上述的逻辑思考:

// 鼠标移入目标元素的操作 
onMounted(async () => { 
  // 鼠标移入目标元素的操作(这里目标仅做了对hover情况下的处理并未做click的处理)
  window.addEventListener('mouseover', mouseoverFn); 
  // 鼠标移出目标元素后的操作   
  window.addEventListener('mouseout', mouseoutFn); 
}); 

const mouseoverFn = (e) => { 
  // 当e.target为目标dom时初始化 
  if (e.target === dom.value) { 
    // init为获取位置信息,计算位置信息,修改提示框属性的函数 
    init(); 
  } 
}; 
const mouseoutFn = (e) => { 
  if (e.target === dom.value) { 
    document.body.removeChild(tooltipContent.value); 
  } 
}; 

上述代码初步完成了鼠标hover目标dom后提示框的渲染与删除 那么此时tooltip组件制作完成了?

想太多..................坑还在后面

踩坑一

使用过其他组件库的人都知道tooltip是可以在目标dom上hover再将鼠标移动到提示框上,提示框是不消失的,当前组件无法达到该效果 想要达到该效果就需要考虑:

  1. 如何在不超出目标dom时就可以将鼠标移入到提示框
  2. 鼠标移入移出是时机控制是怎么样的

第一个问题其实在上面制作小三角的时候就已经解决,因为使用的是伪元素制作,伪元素有一部分在目标dom的范围内,因此鼠标在没有移出目标dom时就可移动到伪元素上(伪元素的特性: 鼠标移动到伪元素上可以等价为移动到了伪元素的父元素上)

第二个问题: 鼠标的移入移出又可以分解为几个小问题:

  1. 鼠标移出目标dom到目标之外非提示框的位置的处理
  2. 鼠标从目标dom直接移入到提示框的处理
  3. 鼠标移出目标dom后再次移入提示框位置的处理

解决问题1: 鼠标在与目标dom交互完成后移出到非提示框的位置,此时页面上不再需要提示框

解决问题2: 鼠标从目标dom直接移入到提示框,此时页面上提示框需保持显示状态

解决问题3: 鼠标移出目标dom后再次移入提示框位置,提示框不显示 (我感觉我好啰嗦.............)

话不多说,上代码了

onMounted(async () => { 
  // 鼠标移入目标元素的操作 
  window.addEventListener('mouseover', mouseoverFn); 
  // 鼠标在目标元素与提示框中移动的操作 
  window.addEventListener('mousemove', mousemoverFn); 
  // 鼠标移出提示框后的操作 
  window.addEventListener('mouseout', mouseoutFn); 
}); 

const mouseoverFn = (e) => { 
  // 当e.target为目标dom时初始化 
  if (e.target === dom.value) { 
    init(); 
  } 
  // tooltipContent.value为提示框 outTarget为鼠标移出时的e.target 
  if (tooltipContent.value) { 
    // 当移入的dom为提示框,且移出的dom为目标dom时,保证提示框显示 
    if (e.target === tooltipContent.value && outTarget.value === dom.value) {
      tooltipContent.value.style.opacity = 1; 
    } else { 
      // 当不满足条件时,提示框不显示,但未删除(这也是上面讲何时删除被推翻) 
      tooltipContent.value.style.transition = 'unset'; 
      tooltipContent.value.style.opacity = 0; 
      // 如果不改变提示框的位置,那么会导致在原地再次移入提示框,提示框再次显示 
      tooltipContent.value.style.left = '-999px'; 
    } 
  } 
};

const mousemoverFn = (e) => { 
  // 因为初始提示框样式为opacity:0 因此在目标dom和提示框中鼠标移动时保持opacity: 1 
  if (tooltipContent.value) { 
    if (e.target === dom.value || e.target === tooltipContent.value) { 
      tooltipContent.value.style.opacity = 1; 
    } 
  } 
};

const mouseoutFn = (e) => { 
  // 当移出的dom为目标dom时,为outTarget赋值 
  if (e.target === dom.value) { 
    outTarget.value = e.target; 
  } 
  // 当移出提示框时删除提示框(何时删除) 
  if (tooltipContent.value && e.target === tooltipContent.value) { 
    document.body.removeChild(tooltipContent.value); 
  } 
}; 

踩坑二

在页面上scroll-y触发时会发生其他目标dom的提示跟随滚动跑到了当前目标dom相对位置上

这里的处理办法是在scroll触发时,将提示框隐藏

// 滚动事件时的操作 
window.addEventListener('scroll', scrollFn); 

const scrollFn = () => { 
  if (tooltipContent.value) { 
    tooltipContent.value.style.opacity = 0; 
  } 
}; 

踩坑三

真边界问题.......... 在实际使用的情况中偶尔会遇到目标dom处于浏览器的边界,此时提示框的显示会根据位置选择是否对内容进行换行,导致的问题就是提示框的高度与宽度变更,倒是之前计算的结果有偏差 正常显示:

边界显示

可以很明显的观察到处于边界时,提示框并没有处于以目标dom中心点的y坐标上,有了一些偏移 这个问题造成的根本原因是提示框渲染逻辑,渲染的逻辑是:

  1. 先根据createElement创建提示框
  2. 将提示框添加到append中
  3. 获取目标dom信息和提示框信息用来计算提示框应该出现的位置
  4. 修改提示框的属性值,将提示框展示在目标位置 因此这里是先获取提示框的宽高再进行移动,在移动后遇到了边界又变更了宽高,但是位置信息并没有变更,因此出现了偏移

代码解决:

const setTooltipStyle = () => { 
  // 计算更改提示框属性的逻辑 
  ...xxxxx 
  // 在更改位置后再次获取最新高度重新赋值 
  currentTooltipHeight.value = tooltipContent.value.offsetHeight; 
}; 
// 对高度进行监听,有变动则重新运行一次修改位置的方法 
watch(() => currentTooltipHeight.value, () => { 
  setTooltipStyle(); 
}); 

以上便是这次构建组件的思路与踩坑,当然,这种处理办法并非是最优的方式,仅供参考(我后面把自己推翻了.....)

------------------------------------完结(累死了自己都晕了)---------------------------------