这是我参与「第五届青训营」伴学笔记创作活动的第 16 天
tooltip组件
tooltip组件和message组件有点点类似,但是这个组件只需要套在别的组件外面即可。实现起来也不难,但是很多坐标处理是比较麻烦的。
接口
接口内容其实很简单,直接看注释即可
import { ExtractPropTypes } from "vue";
export const TooltipType = [
"left-start",
"left",
"left-end",
"top-start",
"top",
"top-end",
"right-start",
"right",
"right-end",
"bottom-start",
"bottom",
"bottom-end",
];
export const TooltipProps = {
placement: { // 摆放位置
type: String,
values: TooltipType,
},
modelValue: { // 响应式渲染显示/隐藏
type: Boolean,
default: null,
},
content: [String, Number], // 文本内容
width: String,
};
export type TooltipProps = ExtractPropTypes<typeof TooltipProps>;
创建节点
首先我们需要在组件中获取到嵌入的节点,这显然就是插槽来做了。再vue我们不能直接用slot,需要在外面套一个标签,这里最好套个span标签,这样也不会影响文档流的布局。
<template>
<span>
<slot></slot>
</span>
</template>
然后我再来解释为什么要获取到嵌入的节点,因为我们实际的实现思路是需要获取到节点的位置,然后在这个节点的位置挂上一个div来渲染的。
首先我们创建一个节点,并给这个节点挂上我们准备好的样式,并把节点内容挂载watch中。
// 获取当前组件的实例
tip = document.createElement("div");
tip.className = `h-tooltip h-tooltip-${placement!.value}`;
width?.value && (tip.style.width = width.value);
tid = tip.id = `h-tooltip-${instance!.uid}`;
watchEffect(() => {
tip.innerHTML = `<span>${content?.value}</span>` || "";
})
渲染节点
紧接着,我们创建完节点后,我们需要考虑如何给这个节点渲染出来并按照特定的位置安放。
由于我们需要操作到节点,在vue异步渲染模式下,我们需要在nextTick中才能获取到,同样需要加入到监听中。
// 核心函数,获取到已经渲染完毕的DOM.
function update(tip: any, tid: string): any {
// 默认取得插件内第一个元素,并获取它的坐标
const Rect = instance?.proxy?.$el.firstElementChild.getBoundingClientRect();
const el = document.getElementById(tid);
if (!el) {
document.body.appendChild(tip);
}
tip.style.display = "block";
const { x, y } = calcStyle(Rect, tip, placement?.value!);
tip.style.top = y + "px";
tip.style.left = x + "px";
}
watchEffect(() => {
tip.innerHTML = `<span>${content?.value}</span>` || "";
nextTick(update(tip, tid));
})
这个渲染函数,就是我们把插槽中的节点获取到,求出坐标系中的坐标,在通过calcStyle获取到对应的节点坐标,挂载在我们创建的节点样式中,最后加入到body后面。
获取坐标位置 & 开发逻辑
坐标获取
位置节点坐标有12种,由 placement 属性决定展示效果: placement属性值为:[方向]-[对齐位置];四个方向:top、left、right、bottom;三种对齐位置:start, end,默认为空。 如 placement="left-end",则提示信息出现在目标元素的左侧,且提示信息的底部与目标元素的底部对齐。
function calcStyle(Rect: any, tip: any, key: string): { x: number; y: number } {
let y = document.documentElement.scrollTop;
let x = 0;
const placement: any = {
"top-start": () => {
x += Rect.x;
y += Rect.y - tip.offsetHeight;
},
top: () => {
x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5;
y += Rect.y - tip.offsetHeight;
},
"top-end": () => {
x += Rect.x + Rect.width - tip.offsetWidth;
y += Rect.y - tip.offsetHeight;
},
"left-start": () => {
x += Rect.x - tip.offsetWidth;
y += Rect.y;
},
left: () => {
x += Rect.x - tip.offsetWidth;
y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5;
},
"left-end": () => {
x += Rect.x - tip.offsetWidth;
y += Rect.y + Rect.height - tip.offsetHeight;
},
"right-start": () => {
x += Rect.x + Rect.width;
y += Rect.y;
},
right: () => {
x += Rect.x + Rect.width;
y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5;
},
"right-end": () => {
x += Rect.x + Rect.width;
y += Rect.y + Rect.height - tip.offsetHeight;
},
"bottom-start": () => {
x += Rect.x;
y += Rect.y + Rect.height;
},
bottom: () => {
x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5;
y += Rect.y + Rect.height;
},
"bottom-end": () => {
x += Rect.x + Rect.width - tip.offsetWidth;
y += Rect.y + Rect.height;
},
};
placement[key]();
return { x, y };
}
显示与隐藏
最后我们需要在hover触发后,才显示节点。这个逻辑就很简单了,我们只需要为节点挂载上两个监听事件mouseenter和mouseleave响应式更新即可。
// 监听有无接触组件,接触了为true,离开为false
el &&
el.addEventListener("mouseenter", () => {
isShow.value = true;
});
el &&
el.addEventListener("mouseleave", () => {
setTimeout(() => {
isShow.value = false;
}, 500);
});
同样的,我们也需要在watch中处理响应式触发的情况。
watchEffect(() => {
...
if (isShow.value) {
show(tip);
} else {
hide(tid);
}
});
// 隐藏tooltip
function hide(tid: string) {
const el = document.getElementById(tid);
if (el) {
// 移除样式
el.classList.remove("h-tooltip-show");
}
}
// 显示tooltip
function show(tip: any) {
tip && tip.classList.add("h-tooltip-show");
}
在实现完成后,我们的页面就会挂载上tooltip节点,但是我们在其它页面不需要它,因此我们需要在实现页面更替时删掉dom原生,防止多个节点出现冗余。
// 退出页面后删除全部的结点
onUnmounted(() => {
const el = document.getElementById(tid);
el && document.body.removeChild(tip);
});
最后实现一个经典的tooltip展示
<template>
<div class="Container">
<div class="row center">
<h-tooltip class="box-item" content="top-start" placement="top-start">
<h-button type="default" plain>top-start</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="top" placement="top">
<h-button type="default" plain>top</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="top-end" placement="top-end">
<h-button type="default" plain>top-end</h-button>
</h-tooltip>
</div>
<div class="row">
<h-tooltip class="box-item" content="left-start" placement="left-start">
<h-button type="default" plain>left-start</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="right-start" placement="right-start">
<h-button type="default" plain>right-start</h-button>
</h-tooltip>
</div>
<div class="row">
<h-tooltip class="box-item" content="left" placement="left">
<h-button type="default" plain>left</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="right" placement="right">
<h-button type="default" plain>right</h-button>
</h-tooltip>
</div>
<div class="row">
<h-tooltip class="box-item" content="left-end" placement="left-end">
<h-button type="default" plain>left-end</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="right-end" placement="right-end">
<h-button type="default" plain>right-end</h-button>
</h-tooltip>
</div>
<div class="row center">
<h-tooltip class="box-item" content="bottom-start" placement="bottom-start">
<h-button type="default" plain>bottom-start</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="bottom" placement="bottom">
<h-button type="default" plain>bottom</h-button>
</h-tooltip>
<h-tooltip class="box-item" content="bottom-end" placement="bottom-end">
<h-button type="default" plain>bottom-end</h-button>
</h-tooltip>
</div>
</div>
</template>
<style>
.Container {
width: 600px;
}
.Container .row {
display: flex;
align-items: center;
justify-content: space-between;
}
.Container .center {
justify-content: center;
}
.Container .box-item {
margin: 10px;
}
</style>
补充
最后附上完整代码地址:
如果觉得有帮助的话,请给我们项目点个star吧。