公共Hooks封装之文字溢出提示useEllipsisPopper
项目环境
Vue3.x + Ant Design Vue3.x + Vite4.x
业务场景分析
图文内容仅供参考,仅提供文章内所需思考对应的图例
在以上图片中,是管理后台系统中常见的表格内容,因使用的是 Ant Design Vue 框架,根据官方的文档中所示: Column
的 API ellipsis
超出宽度自动省略,不支持和排序筛选一起使用,,且表格布局将变成 tableLayout="fixed"
。 实际使用的代码:
[
{
title: "所属角色",
key: "role",
width: 100,
},
{
title: "所在部门",
key: "department",
width: 160,
ellipsis: true,
},
];
从上图中则暴露了一个问题,那就是由于 column
作为“配置项”传入表格组件,对于字数可能较长的字段,配置 ellipsis: true
后,无论文本内容是否超出表格列的宽度,都会渲染出 tooltip
,从体验方面和性能方面来说,都未必好,渲染了一些“无意义”的 DOM。
同样的,在中后台管理系统中,因为业务考虑或 UI 界面设计等等原因,会出现部分显示区域需要显示可能过长的字段内容,而根据技术选型配套的 Ant Design Vue 提供了 tooltip
组件依然有上述问题。
Element Plus 如何做的?
作为前端流行的 UI 框架之一,ElementUI Plus 的表格内容,对于上述场景是怎么做的,我们可以从其文档中找到对应的内容~
在上图中,发现 Element Plus 确实对于表格场景,解决了字段根据是否超出再显示 tooltip 的问题。后根据上述配置进行 demo 验证也发现确实可用。 根据官方文档和仓库中的部分源码,发现一个第三方 js 库
Popper.js
TOOLTIP & POPOVER POSITIONING ENGINE
从官方文档及搜索出来各种教程,不难理解,这是一个扩展性较好的 tooltips 提示类 JS 插件,大小仅为 3.5KB 左右,使用与配置也相当简单,基于 popper.js
封装的组件库也有不少,关于这部分的内容,不作为文章重点,且已经有很多介绍其原理和其他相关的优秀内容,在此,不作赘述~
在了解了这个是干嘛的之后,开始着手写项目中需要的 Hooks, 使用 popper.js
主要用到 createPopper()
方法,其接受了 3 个参数:reference
(需要弹框的按钮 Element)、popper
(tooltip 内容 HTMLElement)以及 options
options
内主要用到了 placement
(方向) 和 modifiers
,Hooks 内用到了name
和offset
,未考虑其他配置参数,更完善和更复杂的一些封装,可以查看Element Plus
或 Tippy.js
等优秀的组件(方法)库。
const popperInstance = createPopper(parent, tooltipContent, {
placement: options.placement ?? "top",
modifiers: [
{
name: "offset",
options: {
offset: [0, 8],
},
},
],
});
封装分解:判断逻辑之宽度计算
在查看了Element Plus
的文档及源码后,发现其仅在Table
组件中有自动省略显示的配置。而对于其他场景,通常我们的做法都是使用tooltip
组件,而这种方式并没有考虑实际内容有没有超出。不能做到动态决定是否显示 tooltip。
Hooks 内的做法则是根据 【 子元素的宽度 + 父元素的 padding > 父元素的宽度 ?展示 tooltip : 不展示】
下面内容是关于实现此 Hooks 的一些部分内容拆解
const getPadding = (el) => {
const style = window.getComputedStyle(el, null);
const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
return {
left: paddingLeft,
right: paddingRight,
top: paddingTop,
bottom: paddingBottom,
};
};
为什么需要获取父元素 Padding ?这里则是关于 BFC 的一些问题,
判断子元素什么时候需要隐藏并展示 tooltip,根据上图,当
Child container
的宽度 + 黄色区域的 padding > Parent container
的宽度后,生成 tooltip.
let range = document.createRange();
range.setStart(target, 0);
range.setEnd(target, target.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
range.detach();
const { left, right } = getPadding(target);
const horizontalPadding = left + right;
document.createRange()
用来创建一个Range
对象,包含了startContainer
和 endContainer
,在这里我们使用 setStart
和 setEnd
来创建选择的 DOM 范围,用来拿到 rangeWidth
以方便后面的比较计算。 在使用范围后,调用 detach()
方法,以便从创建范围的文档中分离出该范围。
关于这部分内容,以及具体的 CSSOM 视图相关的知识,可以查看张鑫旭大佬的文章,文章地址在这:CSSOM 视图模式(CSSOM View Module)相关整理
封装分解:创建 tooltipContent
生成 tooltip 的前置条件判断好了,tooltip 的内容要显示什么,本 Hooks 利用的是鼠标移入时获取自定义属性 data-title
并将其赋值为innerText
,根据 popper.js 的文档,创建tooltipContent
和 arrowContent
。
const renderContent = (target, parent) => {
const tooltipContent = document.createElement("div");
const arrowContent = document.createElement("div");
arrowContent.className = ["ellipsis-tooltip-arrow"].join(" ");
arrowContent.setAttribute("data-popper-arrow", "true");
tooltipContent.innerText = target.dataset.title;
tooltipContent.setAttribute("role", "tooltip");
tooltipContent.appendChild(arrowContent);
tooltipContent.className = ["ellipsis-tooltip"].join(" ");
parent.setAttribute("aria-describedby", "tooltip");
parent.appendChild(tooltipContent);
return {
tooltipContent,
};
};
同样的,在鼠标移出时,销毁 popperInstance、移除鼠标离开的监听事件。
popperInstance.destroy();
parent.removeChild(tooltipContent);
parent.removeAttribute("aria-describedby");
target.removeListener("mouseleave", removePopper);
封装分解:EllipsisPopper.vue 组件
<template>
<div class="ellipsis" :data-title="text" @mouseenter="handleCellMouseEnter">
<span>{{ text }}</span>
</div>
</template>
<script setup>
import { useEllipsisPopper } from "@/hooks";
defineProps({
text: {
type: String,
required: true,
},
});
const { handleCellMouseEnter } = useEllipsisPopper({ placement: "auto" });
</script>
因考虑将管理系统中,除表格之外的其他渲染内容,也统一使用动态展示 tooltip,搭配 Hooks 使用,封装 EllipsisPopper
组件。
至此,便理清了 Hooks 内需要的内容,另外,Hooks 内仅考虑了单行文本溢出隐藏展示 tooltip,对于多行文本溢出隐藏后展示 tooltip 的需求并未考虑,相对应的,也没有实现如 Element Plus
更复杂的配置,Hooks 本身结合项目实际需求而言,未做更复杂的拓展。
最后,贴一下使用 EllipsisPopper
组件和useEllipsisPopper.js
后,文章初始的表格变化吧~
Tips 因实际项目需要兼容生态应用(钉钉、飞书)等,需要根据对应开发平台展示部分企业架构相关的字段,文章所示的
EllipsisPopper
组件仅便于理解,和实际业务组件脱敏处理提取的内容,如果不需要兼容生态应用,则可以直接给父元素(即定款展示区域)自定义属性data-title
,并加上@mouseenter="handleCellMouseEnter"
最后,贴一下完整代码~
useEllipsisPopper.js 完整代码
import { createPopper } from "@popperjs/core";
const getPadding = (el) => {
const style = window.getComputedStyle(el, null);
const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0;
const paddingRight = Number.parseInt(style.paddingRight, 10) || 0;
const paddingTop = Number.parseInt(style.paddingTop, 10) || 0;
const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0;
return {
left: paddingLeft,
right: paddingRight,
top: paddingTop,
bottom: paddingBottom,
};
};
const renderContent = (target, parent) => {
const tooltipContent = document.createElement("div");
const arrowContent = document.createElement("div");
arrowContent.className = ["ellipsis-tooltip-arrow"].join(" ");
arrowContent.setAttribute("data-popper-arrow", "true");
tooltipContent.innerText = target.dataset.title;
tooltipContent.setAttribute("role", "tooltip");
tooltipContent.appendChild(arrowContent);
tooltipContent.className = ["ellipsis-tooltip"].join(" ");
parent.setAttribute("aria-describedby", "tooltip");
parent.appendChild(tooltipContent);
return {
tooltipContent,
};
};
export function useEllipsisPopper(options = {}) {
const handleCellMouseEnter = (event) => {
const target = event.target;
const parent = target.parentNode;
let range = document.createRange();
range.setStart(target, 0);
range.setEnd(target, target.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
range.detach();
const { left, right } = getPadding(target);
const horizontalPadding = left + right;
if (Math.floor(rangeWidth + horizontalPadding) > target.clientWidth) {
const { tooltipContent } = renderContent(target, parent);
const popperInstance = createPopper(parent, tooltipContent, {
placement: options.placement ?? "top",
modifiers: [
{
name: "offset",
options: {
offset: [0, 8],
},
},
],
});
const removePopper = () => {
popperInstance.destroy();
parent.removeChild(tooltipContent);
parent.removeAttribute("aria-describedby");
target.removeListener("mouseleave", removePopper);
};
target.addEventListener("mouseleave", removePopper);
}
};
return {
handleCellMouseEnter,
};
}
需要额外补充的样式至项目内
.ellipsis-tooltip {
z-index: 10;
display: inline-block;
background: #333333;
color: #ffffff;
padding: 5px 10px;
font-size: 13px;
border-radius: 4px;
}
.ellipsis-tooltip-arrow,
.ellipsis-tooltip-arrow::before {
position: absolute;
width: 6px;
height: 6px;
background: inherit;
}
.ellipsis-tooltip-arrow {
visibility: hidden;
}
.ellipsis-tooltip-arrow::before {
visibility: visible;
content: "";
transform: rotate(45deg);
}
.ellipsis-tooltip[data-popper-placement^="top"] > .ellipsis-tooltip-arrow {
bottom: -3px;
}
.ellipsis-tooltip[data-popper-placement^="bottom"] > .ellipsis-tooltip-arrow {
top: -3px;
}
.ellipsis-tooltip[data-popper-placement^="left"] > .ellipsis-tooltip-arrow {
right: -3px;
}
.ellipsis-tooltip[data-popper-placement^="right"] > .ellipsis-tooltip-arrow {
left: -3px;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南