长文本的省略与展开

653 阅读5分钟

文本超出一定长度之后就显示省略符号,是很常见的一个功能和业务需求。省略主要有两种方式,一种是只显示一行,另一种是指定可显示的行数,超出可显示行数的内容就省略,所以现在就整理一下不同要求下的实现方式。

单行文本

常见的处理方法是使用 CSS 的 text-overflow 属性。同样的,还需要组合 white-space 和 overflow 属性以保证效果。

<div class="limitedSpace">
    {'A design language for background applications, is refined by Ant UED Team.'.repeat(20,)}
</div>
.limitedSpace{
     width:200px;           /* 设置父容器宽度 */        
     overflow:hidden;       /* 隐藏溢出内容 */
     white-space: nowrap;   /* 强制文本不换行 */
     text-overflow: clip;   /* 使用省略号表示溢出文本 */
}

其中,text-overflow 用于处理当文本溢出它的容器时如何显示的情况,它的几种常见值:

  • clip 当文本溢出容器时,任何溢出的部分将被简单地剪裁掉,也就是什么都不显示。
  • ellipsis 当文本溢出容器时,使用省略号 (...) 表示被剪裁的文本部分。

效果图👇

image.png image.png

重要!!!

text-overflow 常常需要组合使用 white-space 和 overflow 属性才能生效。

多行文本

方法一:基于CSS

CSS 提供了一个-webkit-line-clamp属性,可以结合其他CSS属性(如display 和 overflow)来实现多行文本的省略。使用 -webkit-line-clamp 时,需要结合 display: -webkit-box 和 -webkit-box-orient

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多行文本省略</title>
<style>
  .multiline-ellipsis {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    -webkit-line-clamp: 3; /* 显示的行数 */
    white-space: normal;
  }
</style>
</head>
<body>
  <div class="multiline-ellipsis">
    这是一个示例文本,它将被限制在三行。如果文本超出了三行,则会显示省略号。此特性在需要处理多行文本的布局时非常有用。这是一个示例文本,它将被限制在三行。如果文本超出了三行,则会显示省略号。此特性在需要处理多行文本的布局时非常有用。这是一个示例文本,它将被限制在三行。如果文本超出了三行,则会显示省略号。此特性在需要处理多行文本的布局时非常有用。
  </div>
</body>
</html>

效果图👇

image.png

方法二:基于js的动态计算

使用css实现省略的方式比较简单,但是不够灵活,也可以基于js来实现。主要思想是计算文本所使用的实际高度是否超过文本容器的高度,如果超过了就省略。样例代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多行文本省略</title>
<style>
  .container {
    width: 300px;
    height: 60px; /* 限制高度 */
    overflow: hidden;
    position: relative;
  }
  .text {
    font-size: 16px;
    line-height: 1.5em;
  }
</style>
</head>
<body>
  <div class="container">
    <div class="text" id="text">
      这是一个示例文本,它将被限制在一个特定的高度内。如果文本超出了这个高度,则会显示省略号。此特性在需要处理多行文本的布局时非常有用。为了使效果更明显,我们添加更长的文本内容来测试。
    </div>
  </div>

  <script>
    function addEllipsis() {
        const container = document.querySelector('.container');
        const textElement = document.querySelector('.text');
        const lineHeight = parseInt(window.getComputedStyle(textElement).lineHeight);
        const maxLines = Math.floor(container.clientHeight / lineHeight);
        const maxHeight = maxLines * lineHeight;

        if (textElement.scrollHeight > maxHeight) {
          let text = textElement.innerText;
          while (textElement.scrollHeight > maxHeight && text.length > 0) {
              text = text.substring(0, text.length - 1);
              textElement.innerText = text + '...';
          }
        }
    }
    window.addEventListener('DOMContentLoaded', (event) => {
        addEllipsis();
    });
  </script>
</body>
</html>

代码包含三部分:

  1. 样式部分:

    • container 设置了固定宽度和高度,并使用 overflow: hidden 来隐藏溢出的内容。
  2. JavaScript 部分:

    • 通过 addEllipsis 函数获取 text 元素的实际高度,并计算出行高和最大行数。
    • 不断减少 text 的内容,直到其高度不再超过 container 的高度,并在文本末尾添加省略号。
    • 由于文末的省略号是手动添加的,因此可以自定义省略方式。
  3. 事件监听器:

    • 通过 DOMContentLoaded 事件监听器,在文档加载完成后执行 addEllipsis 函数。

自定义省略展开、收起组件

现在大多数的项目是基于react框架开发的,因此实现了一个初步的省略组件,该组件可以自定义展开收起的文案。

其主要思想是:先创建一个不展示出来的虚拟元素,并将原始文本作为其innerText,计算该虚拟元素所占的行数是否超过最大行数,如果超过,则依次减少其字数,直到不超过最大行数,得到的新文本作为目标元素的innerText,最后将虚拟元素的innerText设为空。

效果图👇

image.png

image.png

// CusEllipse.tsx
import React, { useState, useRef, useEffect } from 'react';
import './App.css'

interface Props {
  maxLine: number;
  content: string;
  className?: string;
  expandText?: string;   // 展开文案
  collapseText?: string; // 收起文案
  showEllipsis?: boolean;
}

export function E(props: Props) {
  const { maxLine, content, expandText = '展开', collapseText = '收起', showEllipsis = true } = props;
  const [expanded, setExpanded] = useState(false);
  const contentRef = useRef<HTMLDivElement>(null);
  const virtualContentRef = useRef<HTMLDivElement>(null);
  const [showEllipse, setShowEllipse] = useState(showEllipsis);

  const [shownContent, setShownContent] = useState(content);
  const [cutIndex, setCutIndex] = useState(0);

  useEffect(() => {
    const cutIndex = getShownText(content, maxLine) as number;  // 计算每个文本应该截断的位置
    setCutIndex(cutIndex);
  }, [content]);

  useEffect(() => {
    if (expanded) {
      setShownContent(content);
    } else {
      const ele = contentRef.current;
      if (ele) {
        setShownContent(content.slice(0, Number(cutIndex)));
      }
    }
  }, [expanded, content, maxLine, cutIndex])


  const getLength = (rects) => {
    var line = 0, lastBottom = 0;
    for (let i = 0, len = rects.length; i < len; i++) {
      if (rects[i].bottom == lastBottom) {
        continue;
      }
      lastBottom = rects[i].bottom;
      line++;
    }
    return line;
  };

  const getShownText = (content, lineClamp: number) => {
    if (virtualContentRef.current === null || contentRef.current === null) return;

    const span1Rects = virtualContentRef.current.getClientRects(); // 获取元素的初始完整区域
    let h = getLength(span1Rects); //行数
    let shownContent = content;
    let cutIndex = content.length - 1;
    console.log(111, span1Rects)

    if (h <= lineClamp) {
      setShowEllipse(false);
    }

    // 预设函数
    while (h > lineClamp) {
      shownContent = shownContent.slice(0, -1);
      virtualContentRef.current.innerText = shownContent + `...${expandText}`;  // 不变更state 避免引起错误状态更新
      h = getLength(virtualContentRef.current.getClientRects());
      cutIndex--;
    }
    virtualContentRef.current.innerHTML = ''
    return cutIndex;
  };

  return (
    <>
      <span ref={virtualContentRef} className='hiddenSpan' >{content}</span>
      <span ref={contentRef} className='ellipse'>
        {shownContent}
        {showEllipse && !expanded && <span>...</span>}
        {showEllipse && <span className='expandedBtn' onClick={() => setExpanded(!expanded)} > {expanded ? collapseText : expandText}</span>}
      </span>
    </>
  );
}
// index.less
.expandedBtn {
  color: #1667ff;
  cursor: pointer;
  white-space: nowrap;
}

.expandTextBtn{
  color: #2a3aff;
}

// App.tsx
import { CusEllipse } from './CusEllipse';
import './App.css'

function App() {
  return (
    <>
      <div className='card'>
       <E
          maxLine={3}
          content={'ddas dasdasd sadas'.repeat(10)}
        />
      </div>
    </>
  )
}
export default App