提起「文本溢出省略」,想必大家都不陌生,那什么是「标签溢出省略」呢?本文将带你深入了解「标签溢出省略」的实现原理。
1. [What]什么是标签溢出省略
在前端开发中,我们经常会用到下拉选择器组件,例如arco-design 的 Select 组件,在多选模式下,可以通过传入形如maxTagCount
这样的属性来配置超出多少条数据进行截断展示。
但是在实际工作中,我们会发现maxTagCount
并不容易配置,因为它会随着 Select 以及其 Option 的宽度而变化,从而导致以下 2 种(容易让人强迫症发作的)情况:
- 空间明明是足够的、但是被截断了;
- 空间不足、但是没有被截断。
自然而然,我们就会思考是否可以提供类似maxTagCount='auto'
这样的特性,根据标签宽度以及容器宽度对可见标签的数量进行动态的调整,这就是标签溢出省略的常见场景。
综上:根据容器大小,动态调整可见标签的数量,并且把不可见的标签折叠到一个特殊的标签内,就叫做标签溢出省略。
2. [Why]为什么需要溢出省略
- 从组件自身的角度看,溢出省略是为了更合理地利用空间、尽可能多地展示标签;
- 从页面整体的角度看,溢出省略是为了固定某个容器的大小(宽度 or 高度),从而使得页面整体的布局更加美观。
3. [How]如何实现标签溢出省略
实现溢出省略的挑战在于:
- 如何计算可见标签的数量?
- 如何降低计算的复杂度?
3.1 省略的本质在于计算
首先明确几个概念:
- 最后一个可见标签的索引:以下简称"max"
- 标签列表的长度:以下简称"dataLength"
- 内容为+x、hover展示剩余标签列表的标签:以下简称"+x的特殊标签"
假设我们已经计算得出 max,那么就可以按照既定的策略去渲染标签列表:
- 索引为 0 ~ 索引为 max-1,正常渲染;
- 索引为 max:渲染一个特殊标签,内容为+(dataLength - max),hover 在弹出层中渲染剩余的标签列表;
- 其余标签:不在容器中渲染。
3.2 如何计算
以从左到右(水平) 排列为例说明计算方式。
3.2.1 渲染标签列表
- 在渲染第一个标签的同时,渲染一个不可见的容器(以下简称hidden-list),设置其内部为弹性布局:主轴为水平方向、起点在左侧;
- 在hidden-list内部,按顺序遍历列表并渲染出所有的标签。
这样做有2️⃣个目的:
- 能够让hidden-list一直存在于DOM树中,响应标签以及容器的变化;
- 能够让hidden-list在水平方向上的起点与第一个标签相同
接下来,我们就可以利用这个hidden-list计算第几个标签超出了容器的宽度
3.2.2 计算第几个标签超出了容器的宽度
在弹性布局中,我们可以通过flex-wrap
来设置内部元素超出一行时是否换行
因此,我们就会有 2 种方案来判断标签是否超出了容器的宽度
方案 1:不换行,比较标签的左偏移量(offsetLeft)和容器宽度(clientWidth)之间的大小。
遍历hidden-list(从索引为 1 开始),若索引为 x-1 的标签的左偏移量小于容器宽度、索引为 x 的标签的左偏移量大于容器宽度,就能得出 max=x-2
方案 2:换行,比较标签的顶部偏移量(offsetTop)和第一个标签高度(clientHeight)之间的大小。
遍历hidden-list,若索引为 x 的标签的顶部偏移量大于第一个标签的高度,就能得出 max=x-1。
3.2.3 调整渲染顺序
完成上面 2 个步骤以后,我们就得出了 max 的值,但实际上我们并没有考虑到 +x 这个特殊标签。
如果在计算得出max后尝试用+x替换Label n,就会遇到以下2个问题:
问题1️⃣:+x和Label n的宽度不一定相同,增加了计算的复杂度。
解决方案:避免比较+x和Label n之间的宽度大小,直接把这个特殊标签放到hidden-list的第1位进行渲染。
问题2️⃣:+1和+99的宽度并不相同,增加了计算的复杂度。
解决方案:提前确定+x的宽度,直接把+x 的 x 设置成它的最大值(dataLength)。
3.2.4 完善渲染策略
在第一次渲染时,显然我们还不知道 max 的值,因此我们可以设置 max 的初始值为 undefined。
❗实际上,还有一种非常极端的情况是:hidden-list第一行只能容纳第一个标签。
这种情况下,计算得出 max = 0,也就是说:当 max = 0 时,只需要渲染一个特殊标签即可。
渲染策略总结如下:
-
索引为 0
- 若 max 不为 0,渲染第一个标签以及hidden-list
- 否则,渲染特殊标签,内容为+(dataLength - max)
-
若 max 为 undefined,返回 null
-
若索引为 max,渲染特殊标签,内容为+(dataLength - max)
-
若索引大于 max,返回 null
-
其他情况(索引在 0 到 max 之间),正常渲染对应索引的标签即可
3.2.5 关键代码
在了解了计算方法以后,我们就可以拆分出以下几个关键的函数:
renderFinalItem
渲染正常的标签
TypeScript
function renderFinalItem(item) {
return <span>{item}</span>
}
renderSpecialItem
渲染特殊的标签
TypeScript
function renderSpecialItem() {
return (
<Popover>
+{list.length - max}
</Popover>
)
}
renderHiddenList
渲染不可见的容器(以及标签列表)
TypeScript
function renderHiddenList() {
return (
<Space
ref={hiddenListRef}
style={{ width: "100%" }}
className="hidden-list-container"
>
<span>+{dataLength}</span>
{list.map((item) => renderFinalItem(item))}
</Space>
);
}
renderList
渲染标签列表
TypeScript
function renderList(list) {
return list.map((item, index) => {
if (index === 0) {
if (max === 0) {
return renderSpecialItem()
}
return (
<>
{renderFinalItem(item)}
{renderHiddenList()}
</>
)
}
if (typeof max === 'undefined') {
return null
}
if (index === max) {
return renderSpecialItem()
}
if (index > max) {
return null
}
return renderFinalItem(item)
})
}
compute
计算 max 的值(基于方案 2)
TypeScript
function compute() {
if (!hiddenListRef.current) {
return
}
const hiddenList = hiddenListRef.current
const childNodeList = [].slice.call(hiddenList.children)
const firstChildHeight = childNodeList[0].clientHeight
for(let i = 1; i < childNodeList.length; i++) {
if (childNodeList[i].offsetTop >= firstChildHeight) {
const lastVisibleIndex = Math.max(i - 1, 0)
setMax(lastVisibleIndex)
return
}
}
setMax(list.length)
}
3.3 何时计算
- 初始化时,需要计算
- 标签列表变化时,需要计算 -> 监听列表的变化
- 容器宽度发生变化时,需要计算 -> 监听容器的 resize 事件
代码示例如下:
TypeScript
function OverflowItems() {
const containerRef = useRef()
useEffect(() => {
compute()
}, [ list ])
useEffect(() => {
const container = containerRef.current
if (!container) {
return
}
const handleResize = () throttle(() => {
compute()
}, 100)
const observer = new ResizeObserver(handleResize)
observer.observe(container)
return () => {
observer.unobserve(container)
}
}, [])
return (
<Space ref={containerRef}>{renderList()}</Space>
)
}
🎉让我们用codesandbox来看看最后的效果。
3.4 拓展思考
3.4.1 从上到下(竖直)排列
其实思路是完全一致的,我们依然有 2 种选择:
方案1:不换行,比较标签的顶部偏移量(offsetTop)与容器高度(clientHeight)之间的大小。
方案2:换行,比较标签的左偏移量(offsetLeft)与第一个标签宽度(clientWidth)之间的大小。
3.4.2 超出n行再省略
在文本溢出省略的场景中,默认都是在"超出 1 行"时进行省略,但在arco-design 的 Typography 组件中,我们还可以通过rows
来配置超出多少行进行省略:
那在标签溢出省略中,基于方案 2(换行排列),我们也可以支持类似的特性。
以"超出 2 行再省略"为例:
- 记录第一行第一个标签的高度(firstRowHeight);
- 记录第二行第一个标签的高度(secondRowHeight);
- 若索引为 x 的标签的顶部偏移量大于 firstRowHeight 和 secondRowHeight 之和,就能得出 max = x - 1。
4. 总结
「标签溢出省略」的挑战和实现总结如下:
- 如何计算可见标签的数量?
通过渲染一个不可见的标签列表hidden-list,提前确定每个标签的位置,再通过比较标签和容器的相对位置来计算可见标签的数量。
- 如何降低计算的复杂度?
在渲染hidden-list时,通过以下2个手段来降低计算的复杂度:
- 把+x放到列表第1位,从而避免+x和Label n的宽度比较;
- 把x设置成它的最大值(dataLength),从而提前确定+x的宽度。