前端文本溢出打点省略展示

2,516 阅读13分钟

前言

绘制页面内容时上经常会遇到「文本内容超出容器打点省略展示」的需求,而展示省略也分多种场景和形式,今天我们来聊一聊这些细节。

  1. 单行文本溢出打点省略(CSS)
  2. 多行文本溢出打点省略(CSS)
  3. 单行/多行文本溢出在 末尾 位置打点省略(JS)
  4. 单行/多行文本溢出在 中间 位置打点省略(JS)
  5. 单行文本溢出在 中间 位置打点省略(CSS)
  6. 超长元素块整块打点省略(CSS)
  7. 自定义打点省略标签(JS)
  8. 元素块在开头,文本围绕它多行省略(CSS)
  9. 元素块在末尾,文本围绕它多行省略(CSS)

一、单行文本溢出打点省略(CSS)

CSS 属性支持对单行文本超长时在 尾部 进行打点省略。为文本容器设置以下属性:

<div class="text-container">
  这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。
</div>
    
.text-container{
  /* 明确指定容器的宽度 */
  width: 200px;
  /* 强制将文字排列在一行,不进行换行 */
  white-space: nowrap;
  /* 超出隐藏 */
  overflow: hidden;
  /* 在文本溢出时,显示省略符号来代表被修剪的文本 */
  text-overflow: ellipsis;
}

image.png

二、多行文本溢出打点省略(CSS)

对于多行文本的超长省略,可使用 -webkit-line-clamp 来指定显示文本的行数,比如下面 两行文本超出进行打点省略。

.text-container{
  /* 明确指定容器的宽度 */
  width: 200px;
  /* 超出隐藏 */
  overflow: hidden;
  /* 限制文本展示的行数,并在超出后末尾显示 省略号 */
  -webkit-line-clamp: 2;
  /* 结合 -webkit-line-clamp 使用,将对象作为弹性伸缩盒子模型显示 */
  display: -webkit-box;
  /* 设置伸缩盒对象的子元素的排列方式 */
  -webkit-box-orient: vertical;
}

但它的短板在于 兼容性一般-webkit-line-clamp 属性只有 WebKit 内核的浏览器才支持,多适用于移动端页面(移动设备浏览器更多是基于 WebKit 内核)。

image.png

三、单行/多行文本溢出在 末尾 位置打点省略(JS)

image.png

选用 JS 来实现更多的是要解决 CSS 多行文本超出省略 的兼容问题,这里的实现同样适用于 单行文本超出省略。

其原理是根据文本字符的长度,根据容器 font-size 大小来计算能够容纳多少文本字符,若无法全部容纳则对文本进行截断展示。

<style>
  .text-container{
    display: block;
    width: 200px;
    line-break: anywhere;
  }
</style>

<div class="text-container"></div>

<script>
  // const text = 'this is a long text. this is a long text. this is a long text. this is a long text. this is a long text. this is a long text. this is a long text. ';
  const text = '这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。这是一段超长的文本。';
  const lineNum = 2;
  const container = document.querySelector('.text-container');

  let { width, fontSize } = window.getComputedStyle(container);
  width = +width.slice(0, -2); // 拿到的值是 string 带 px,这里转成 number
  fontSize = +fontSize.slice(0, -2);

  // 1. 按中英文计算文本字符长度(中文两个字符,英文)
  const textLength = computedTextLength(text);
  // 2. 计算容器一行所能容纳的字符数
  const lineCharNum = Math.floor(width / fontSize) * 2;
  // 多行可容纳总字数
  const totalStrNum = Math.floor(lineCharNum * lineNum);

  // 内容截取
  let content = '';
  if (textLength > totalStrNum) {
    const lastIndex = totalStrNum - textLength;
    content = sliceTextLength(text, totalStrNum - 3).concat('...'); // ... 代表三个字符
  } else {
    content = text;
  }

  container.innerHTML = content;
</script>

computedTextLengthsliceTextLength 会将 中文 按照两个字符计算。

const computedTextLength = text => {
  let length = 0;
  for (let i = 0; i < text.length; i ++) {
    if (text.charCodeAt(i) < 0 || text.charCodeAt(i) > 255) {
      length += 2;
    } else {
      length += 1;
    }
  }
  return length;
}

const sliceTextLength = (text, sliceLength) => {
  let length = 0, newText = '';
  for (let i = 0; i < text.length; i ++) {
    if (text.charCodeAt(i) < 0 || text.charCodeAt(i) > 255) {
      length += 2;
    } else {
      length += 1;
    }
    if (length <= sliceLength) {
      newText += text[i];
    } else {
      break;
    }
  }
  return newText;
}

同样,JS 实现也存在短板:省略号展示的位置存在计算偏差(一个 font-size 的宽度对应两个字符存在一定偏差(字母 和 空格)),并不会像 CSS 能够将省略展示位置刚刚好。

为了避免相邻的 字符与符号 在末尾被强制换行展示(尽管末尾可以容纳下字符,但容纳不下符号),造成展示区域的浪费,建议设置 line-break: anywhere;

四、单行/多行文本溢出在 中间 位置打点省略(JS)

image.png

这类需求常见的场景是对 超长的文件名称进行中间位置省略,这样可以展示出文件后缀类型。

CSS 对超长文本省略的打点位置只能在末尾,采用 JS 实现可以控制省略打点位置,我们改造一下上面 「单行/多行文本超长打点省略(JS)」 截取逻辑。

...
let content = "";
if (textLength > totalStrNum) {
  // const lastIndex = totalStrNum - textLength;
  // content = sliceTextLength(text, totalStrNum - 3).concat('...'); // ... 代表三个字符

  // 如果只有一行 lineNum = 1,这里可直接截取一半,拼接 ...
  // const middleIndex = Math.floor(totalStrNum / 2);
  // content = `${sliceTextLength(text, middleIndex)}...${lastSliceTextLength(text, middleIndex - 3)}`;

  // 当出现多行时,我们只在最后一行的中间位置拼接 ...
  const middleIndex = Math.floor(totalStrNum * ((lineNum * 2 - 1) / (lineNum * 2)));
  // 左边腾出两个字符,右边腾出一个字符 拼接省略 ...
  content = `${sliceTextLength(text, middleIndex - 2)}...${lastSliceTextLength(text, totalStrNum - middleIndex - 1)}`;
} else {
  content = text;
}
...

const lastSliceTextLength = (text, sliceLength) => {
  let length = 0, newText = '';
  for (let i = text.length - 1; i > 0; i --) {
    if (text.charCodeAt(i) < 0 || text.charCodeAt(i) > 255) {
      length += 2;
    } else {
      length += 1;
    }
    if (length <= sliceLength) {
      newText = text[i] + newText;
    } else {
      break;
    }
  }
  return newText;
}

五、单行文本溢出在 中间 位置打点省略(CSS)

image.png

思路:
1)使用 before 伪元素将文本复制一份;
2)设置 width: 50% 占据一半的空间,并通过 float 排列在容器右侧;
3)设置文本溢出隐藏;
4)设置 direction: rtl 改变排版方向,让文字从右到左展示,并将省略号排列在左侧。

为了实现文本超出容器时再进行省略展示,我们结合 JS 来判断文本是否超出容器。

HTML 和 CSS 部分:

<div class="text">秋天,这个金色的季节,在大自然的画卷中显得格外美丽。在这个时候,大地变得充满诗意,人们心灵也沉浸在无限的美好之中。</div>

<style>
  * {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
  }

  .text {
    width: 500px;
    height: 28px;
    line-height: 24px;
    border: 2px solid lightblue;
    border-radius: 4px;
    margin: 300px auto;
    /* 设置 overflow: hidden; 超出一行的数据进行隐藏 */
    overflow: hidden;
  }

  .text.text-middle-ellipsis::before {
    content: attr(title);
    /* 1、给伪元素设置右浮动,宽度占 50% */
    width: 50%;
    float: right;
    /* 2、设置超出截断 */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* 3、改变排版方向,让文字从右到左展示,并将省略号排列在左侧 */
    direction: rtl;
    /* 4、省略号和前面的文字间距过大,可以适当设置 margin-left: - 值,减少左侧空白区域多渲染一个文字 */
    margin-left: -10px;
  }
</style>

JS 部分:

<script>
  const text = document.querySelector(".text");
  const textContent = text.innerText;
  if (text.scrollHeight > text.clientHeight) {
    // 文本超出容器(内容高度 > 一行),设置为中间省略展示
    text.setAttribute("title", textContent);
    text.classList.add("text-middle-ellipsis");
  }
</script>

六、超长元素块整块打点省略(CSS)

上面涉及的场景都是容器内渲染纯文本进行省略展示,假如容器内是行内块元素(业务上的数据标签),如何实现一行排不下时整个标签被移除进行打点省略展示呢(非 标签展示了一部分被截取)?

容器内 DOM 结构如下:

<div class="container">
  <span class="tag">前端</span>
  <span class="tag">后端</span>
  <span class="tag">测试</span>
  <span class="tag">UI</span>
  <span class="tag">产品</span>
</div>

要实现省略需要两步操作:

第一步,为容器设置内容超出打点省略:

.container{
  width: 200px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

第二步,将 span 元素设置为 display: inline-block,这样才能实现标签在排不下时,将标签整块都省略,而不是让标签只显示一部分打点省略。

.tag{
  display: inline-block;
  padding: 2px 4px;
  border-radius: 2px;
  background: purple;
  color: #fff;
  margin-right: 4px;
}

效果图如下:

image.png

但是经过测试会发现 iOS 及 safari 浏览器下标签并未按照整块进行省略,解决办法是使用 多行省略替代单行省略

.container{
  width: 200px;
  /* white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis; */

  /* 注意这里使用 normal */
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
}

七、自定义打点省略标签(JS)

现在基于 需求五 进行了升级,期望 ... 能够可操作:移入或点击 能够查看所有 tag。类似下方示意图,需要我们将 打点省略 作为一个元素去实现。

image.png

这种情况我们需要采用 JS 来实现。首先渲染节点和样式,

<style>
  *{
    padding: 0;
    margin: 0;
  }
  .container{
    margin: 100px auto;
    width: 180px;
    /* 容器要指定高度 */
    height: 28px;
    /* 容器要设置相对定位 */
    position: relative;
  }
  .tag{
    display: inline-block;
    padding: 2px 4px;
    border-radius: 2px;
    background: purple;
    color: #fff;
    margin-right: 4px;
  }
  .tag:last-child{
    margin-right: 0;
  }
</style>
<div class="container"></div>

<script>
  const createTagEl = text => {
    const span = document.createElement('span');
    span.className = "tag";
    span.innerHTML = text;
    return span;
  }
  const tags = ['前端', '后端', '测试', 'UI', '产品'];
  const container = document.querySelector('.container');
  tags.forEach(tag => container.appendChild(createTagEl(tag)));
</script>

然后根据标签内容动态计算是否需要添加 打点省略 元素(可参考代码注释)。

...
const ellipsisWidth = 22 + 4; // 假设省略打点元素的宽度为 22,左边距为 4
const { offsetWidth, clientHeight, scrollHeight, children } = container; // 容器要指定宽高

// 需要打点省略
if (scrollHeight > clientHeight) {
  // 找到第一个被换行的 tag 索引(基于距离父元素的 偏移量 来计算)
  let sliceEndIndex = Array.from(children).findIndex(child => child.offsetTop !== 0);

  // 如果第一行排不下 ellipsis tag,调整 sliceEndIndex 索引
  while (sliceEndIndex > 0) {
    const child = children[sliceEndIndex - 1];
    // 剩余的空间可以用于展示 ellipsis tag
    if (offsetWidth - (child.offsetWidth + child.offsetLeft) >= ellipsisWidth) {
      break;
    }
    sliceEndIndex --; // 要删除标签
  }

  // 删除排不下的 tag
  let length = children.length - sliceEndIndex;
  while (length > 0) {
    container.removeChild(children[children.length - 1]);
    length --;
  }
  // 创建 ellipsis tag
  container.appendChild(createTagEl('...'));
}

八、元素块在开头,文本围绕它多行省略(CSS)

在网页中,左上角展示图片,文字围绕图片展示是比较常见的需求,通过对图片设置 float: left 浮动来实现。

如果要实现文字围绕元素块且还要实现打点省略,如下图所示:

image.png

可以把数字标志看做是一个 inline-block 行内块元素(可手动设置 display: inline-block 或者使用 float: left),结合打点省略属性来实现:

.container{
  width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  /* 可选:设置换行规则 */
  word-break: break-word;
  /* 需要强制换行,一般用于 link 链接 */
  /* word-break: break-all; */
}
span{
  display: inline-block;
  /* float: left; */
  ... 其他样式属性
}

<div class="container">
  <span>1</span>
  这是围绕的文本这是围绕的文本这是围绕的文本这是围绕的文本这是围绕的文本这是围绕的文本这是围绕的文本这是围绕的文本
</div>

九、元素块在末尾,文本围绕它多行省略(CSS)

与上面「元素块在开头,文本围绕它多行省略」不同,它的 元素块 是在多行省略的尾部。如常见需求:实现展开/收起:

image.png

1、实现思路

我们需要考虑如何让「元素块」摆放在「多行文本省略」末尾。具体步骤如下:

  1. 元素块 的 书写位置 要在 文本 前面;
  2. 设置 float: right 将 元素块 摆放到右侧;
  3. 让 元素块 摆放到右下角位置:定义一个浮动元素(.text::before)并设置 宽度为 0 和 一定的高度值,将 元素块 挤下去(元素块同时需设置清除浮动 clear: both);
  4. 固定高度:新的浮动元素可以通过我们计算来设置一个固定像素,如:height: 24px
  5. 动态高度:也可设置 height: calc(100% - 元素块高度) 适配任何多行文本场景。但需要新增一个外层容器 .wrap 并设置 display: flex 布局,目的是让子项(.text::before)的 height: 100% 有效,而非得到 0px;
  6. 最后,为 .text 设置 display: -webkit-box 等 CSS 多行文本省略属性。
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <style>
      .wrap {
        display: flex;
      }

      .text {
        width: 420px;
        line-height: 24px;
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
      }

      .text::before {
        content: "";
        float: right;
        width: 0;
        /* height: 24px; */
        height: calc(100% - 24px);
        background: red;
      }
      
      button{
        /*基础装饰样式*/
        background: #3f51b5;
        cursor: pointer;
        color: #fff;
        font-size: 14px;
        padding: 0 2px;
        border-radius: 2px;
        outline: none;
        border: none;
        height: 24px;
        line-height: 24px;
      }

      .btn {
        position: relative;
        float: right;
        clear: both;
      }
    </style>
  </head>
  <body>
    <div class="wrap">
      <div class="text">
        <button class="btn">展开</button>
        伴着天空小鸟的嘲杂声,我们到了山头,清新的空气迫不及待飘到了我身边,让我更加神情气爽,心旷神怡,空气中,似乎掺杂着一丝花香,俯看山下的景色,美不胜收,绿油油的一片,我不禁为这种绿而感叹,这种绿的境界,可以说世界上的任何一种颜料都难于它媲美,小草软绵绵的,同时也湿辘辘的,这兴许是春雨的‘杰作’吧。我走着走着,一缕缕花香从远方传来,我走近一看,数十支娇艳的迎春花紧紧的依偎在一起,在春风的吹拂下它们翩翩起舞,显的别有一番风韵。
      </div>
    </div>
  </body>
</html>

2、兼容处理

到这里,在 Chrome 下一切查看一切正常,但在 Safari 浏览器下,设置 display: -webkit-box 会导致布局错乱无法实现我们期望的效果。

因此,为了能够兼容所有浏览器,使用 模拟打点省略 代替 CSS display: -webkit-box,其他的逻辑保持不变。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <style>
      .wrap {
        display: flex;
      }

      .text {
        width: 420px;
        line-height: 24px;
        overflow: hidden;
-       display: -webkit-box;
-       -webkit-line-clamp: 2;
-       -webkit-box-orient: vertical;
+       line-height: 1.5;
+       max-height: 3em;
      }

      .text::before {
        content: "";
        float: right;
        width: 0;
        /* height: 24px; */
        height: calc(100% - 24px);
        background: red;
      }
      
      button{
        /*基础装饰样式*/
        background: #3f51b5;
        cursor: pointer;
        color: #fff;
        font-size: 14px;
        padding: 0 2px;
        border-radius: 2px;
        outline: none;
        border: none;
        height: 24px;
        line-height: 24px;
      }

      .btn {
        position: relative;
        float: right;
        clear: both;
+       margin-left: 20px;
      }

+     .btn::before {
+       content: "...";
+       position: absolute;
+       left: -5px;
+       color: #333;
+       transform: translateX(-100%);
+     }
    </style>
  </head>
  ...
</html>

3、动态控制 元素块 的显示

完善:在没有出现溢出情况时,我们期望不展示 元素块 或 元素块 不右浮动而是紧跟文字其后,这时需要使用 JS 来控制实现。

下面我们通过 JS 来实现:省略展示时,元素块 展示在右下角;没有溢出省略时,元素块 紧随 文本 其后

  1. 改变布局,默认视为没有溢出省略,元素块 书写位置 在文本后面;
  2. 当 JS 判断出现文本溢出省略时,将 元素块 移动到 文本前面,并添加布局 .btn class 完成右下角的展示。
<div class="wrap">
  <div class="text">
    伴着天空小鸟的嘲杂声,我们到了山头,清新的空气迫不及待飘到了我身边,让我更加神情气爽,心旷神怡,空气中,似乎掺杂着一丝花香,俯看山下的景色,美不胜收,绿油油的一片,我不禁为这种绿而感叹,这种绿的境界,可以说世界上的任何一种颜料都难于它媲美,小草软绵绵的,同时也湿辘辘的,这兴许是春雨的‘杰作’吧。我走着走着,一缕缕花香从远方传来,我走近一看,数十支娇艳的迎春花紧紧的依偎在一起,在春风的吹拂下它们翩翩起舞,显的别有一番风韵。
    <button>展开</button>
  </div>
</div>

<script>
  const text = document.querySelector(".text");
  const btn = document.querySelector("button");
  // 处于溢出省略展示
  if (text.scrollHeight > text.clientHeight) {
    // 将 btn 移动到 文本 前面
    text.insertBefore(btn, text.firstChild);
    // 为 btn 添加布局 class
    btn.classList.add("btn");
  }
</script>

当然,如果 .text 宽度是随窗口自适应,你还可以监听 window.resize 窗口事件,动态调整 元素块 的布局。

参考

1、小技巧!CSS 整块文本溢出省略特性探究
2、可能是最全的 “文本溢出截断省略” 方案合集
3、CSS 实现多行文本“展开收起”
4、CSS 文本超出提示效果