手把手教你如何实现「标签溢出省略」

提起「文本溢出省略」,想必大家都不陌生,那什么是「标签溢出省略」呢?本文将带你深入了解「标签溢出省略」的实现原理。

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 渲染标签列表

  1. 在渲染第一个标签的同时,渲染一个不可见的容器(以下简称hidden-list),设置其内部为弹性布局:主轴为水平方向、起点在左侧;
  2. 在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. 总结

「标签溢出省略」的挑战和实现总结如下:

  1. 如何计算可见标签的数量?

通过渲染一个不可见的标签列表hidden-list,提前确定每个标签的位置,再通过比较标签和容器的相对位置来计算可见标签的数量。

  1. 如何降低计算的复杂度?

在渲染hidden-list时,通过以下2个手段来降低计算的复杂度:

  • 把+x放到列表第1位,从而避免+x和Label n的宽度比较;
  • 把x设置成它的最大值(dataLength),从而提前确定+x的宽度。
相关推荐
小牛itbull3 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet15 小时前
那总结下来,react就是落后了
前端·react.js
王解18 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语1 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情1 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0011 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解1 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull2 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress
~甲壳虫2 天前
react中得类组件和函数组件有啥区别,怎么理解这两个函数
前端·react.js·前端框架
用户8185216881172 天前
react项目搭建create-router-dom,redux详细解说
react.js