前言
这篇文章来讲一讲我在kits-tooltip组件构建过程中遇到的一些问题,可以分为一下几点
- 结构问题
- 位置与方向问题
- 边界问题
结构问题
因为要做的是一个组件库中的组件,所以使用的方式无非是采用与其他组件库相似的包裹式使用法
<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是这么介绍的:
也就是说通过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的位置显示 这又引出了新的问题:
- 目标dom的位置信息获取(相对于视口的信息)
- 提示框相对于目标dom的位置计算
- 小三角的位置
目标dom的位置信息
目标dom的位置信息是相对好获取的,在这里推荐一个api: getBoundingClientRect() getBoundingClientRect() 返回的是矩形的集合,表示了当前盒子在浏览器中的位置以及自身占据的空间的大小,除了 width 和 height 以外的属性是相对于 视图窗口的左上角 来计算的.因此使用该api获取相对视口的信息最为合适
提示框的位置
提示框位置要根据目标dom的位置来进行计算,实际上只需要目标dom的left, top, width和 height即可计算出提示框的位置.
以上边的提示框的top计算为例:
由此可以得出提示框的top信息,以此类推可以算出提示框的left信息,这时提示框的位置信息就确定了
其他方向的提示框也是同样的分析方式得出计算公式来进行计算
小三角的位置
小三角的位置不同可表示tooltip方向不同,在当前组件中仅构思了上下左右四个方向
小三角采用伪元素的boder来构建,这样做的好处,在下一个问题中进行详细解答
小三角的位置采用了css来进行处理,不同的位置使用k-tooltip传入的position属性设置不同的类名来达到效果
边界问题
在开发过程中,tooltip的实现相对来说是比较简单的,但是需要考虑的边界条件也不少:
- 窗口缩放时提示框跟随目标dom
- 页面滚动时提示框的处理
- 提示框的渲染与删除
- 鼠标从目标dom移动到提示框上,保持提示框不消失
- 在屏幕边界时,提示框的宽高位置重载
窗口缩放时提示框跟随目标dom
在窗口缩放时因为改变了目标dom的相对位置,因此会出现问题,由于使用了相对位置进行计算,所以该问题已经得到了解决
页面滚动时提示框的处理
在页面中有滚动条时,提示框会根据滚动的高度去改变自身top的数,使显示发生偏移
这个问题是由于定位的处理不当,在这里需要将提示框的定位修改为固定定位,即可解决问题
提示框的渲染与删除
提示框的渲染与删除是一个比较大的问题,这里需要思考的是:
- 何时渲染
- 渲染的方式
- 何时删除
何时渲染很好解决,在对目标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再将鼠标移动到提示框上,提示框是不消失的,当前组件无法达到该效果 想要达到该效果就需要考虑:
- 如何在不超出目标dom时就可以将鼠标移入到提示框
- 鼠标移入移出是时机控制是怎么样的
第一个问题其实在上面制作小三角的时候就已经解决,因为使用的是伪元素制作,伪元素有一部分在目标dom的范围内,因此鼠标在没有移出目标dom时就可移动到伪元素上(伪元素的特性: 鼠标移动到伪元素上可以等价为移动到了伪元素的父元素上)
第二个问题: 鼠标的移入移出又可以分解为几个小问题:
- 鼠标移出目标dom到目标之外非提示框的位置的处理
- 鼠标从目标dom直接移入到提示框的处理
- 鼠标移出目标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坐标上,有了一些偏移 这个问题造成的根本原因是提示框渲染逻辑,渲染的逻辑是:
- 先根据createElement创建提示框
- 将提示框添加到append中
- 获取目标dom信息和提示框信息用来计算提示框应该出现的位置
- 修改提示框的属性值,将提示框展示在目标位置 因此这里是先获取提示框的宽高再进行移动,在移动后遇到了边界又变更了宽高,但是位置信息并没有变更,因此出现了偏移
代码解决:
const setTooltipStyle = () => {
// 计算更改提示框属性的逻辑
...xxxxx
// 在更改位置后再次获取最新高度重新赋值
currentTooltipHeight.value = tooltipContent.value.offsetHeight;
};
// 对高度进行监听,有变动则重新运行一次修改位置的方法
watch(() => currentTooltipHeight.value, () => {
setTooltipStyle();
});
以上便是这次构建组件的思路与踩坑,当然,这种处理办法并非是最优的方式,仅供参考(我后面把自己推翻了.....)
------------------------------------完结(累死了自己都晕了)---------------------------------