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

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

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的宽度。
相关推荐
第七玩家6 小时前
React-异步队列执行方法useSyncQueue
前端·javascript·react.js
IT、木易14 小时前
大白话react第十六章React 与 WebGL 结合的实战项目
前端·react.js·webgl
市民中心的蟋蟀17 小时前
第十六章 React中常用的的错误处理方法 【下】
前端·javascript·react.js
ffiyu17 小时前
【React进阶系列第三课】组件函数什么时候被执行
前端·react.js
ffiyu17 小时前
【React进阶系列第二课】JSX 与 React Element
前端·react.js
ffiyu17 小时前
【React 进阶系列第一课】组件就是函数
前端·react.js
市民中心的蟋蟀19 小时前
第十六章 React中常用的的错误处理方法 【上】
前端·javascript·react.js
Bigger19 小时前
useEffect 的底层是如何实现的?(美团面试原题)
前端·react.js·面试
winyh520 小时前
从零开始封装React UI 组件库并发布到NPM
前端·react.js·前端框架
蠟筆小新工程師1 天前
React Native 建構apps的好處在哪裡
javascript·react native·react.js