在前端领域,谈到文本截断,很多人第一反应可能是text-overflow: ellipsis这行代码。我也一样,而且这是我心目中最为简单和优雅的实现方式了,不过,现实之中总能遇到一些意外状况。比如,多行文本的截断怎么操作? 如果说这还有现成的属性可以用,那更麻烦一点的,如果截断操作不在DOM中,比如在canvas中,怎么办?

如果上面这两个问题你脑海中已经有答案了,那你或许不用浪费时间看下去了,为师已经没什么可教的了。否则,看完本文,相信你能学到一点新东西。

我最近在使用cytoscape.js画图的时候,就遇到了这样的场景,需要在canvas中做多行文本的截断,尽管这个库已经支持了不少样式操作,但是很遗憾,并不支持多行文本截断。不过这个需求并非不可实现的,借助canvas的measureTextAPI,再结合javascript,还是可以实现的。

单行文本截断

在具体描述我的解决方案之前,先复习和回顾一下CSS实现的文本截断及其特点。

要实现单行文本截断,text-overflow: ellipsis一行代码是不够的,比如结合其他几个属性一起使用,其中包括:

  1. overflow: hidden; 这是为了让溢出的部分隐藏的,不添加这个属性的话,截断的省略号是看不到的
  2. white-space: nowrap; 这会强制不让文本换行,因为text-overflow只能操作单行文本
  3. widthmax-width 用于限制宽度,否则浏览器也不知道什么时候该截断。不过这个属性并不一定必须加在目标元素身上,其父元素或祖先元素有也可以,只要该元素的宽度被限制就够了。
  4. display 这个属性也不是必须的,因为宽度对于行内元素是没用的,所以如果目标元素是span的话,需要这个属性将其变为非行内元素,例如display: inline-block;

一段完整的CSS代码可能是这样的

1
2
3
4
5
6
7
.ellipsis {
  width: 200px;
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

被截断的元素还有几个特点:

  1. 文本省略是纯样式操作,完整的文本仍然在DOM中,当用鼠标三击选中整句话,然后复制,得到的是完整文本。
  2. 这个符号,是Unicode字符U+2026,并非三个英文句号,且这个符号是无法选中的。
  3. 不管是max-width还是width,只要文本溢出省略号出现,那么这一个元素的宽度就是指定的数值。这里举个例子解释下,假如max-width是200px,截断后的文本是192px,省略符号是6px,按理说元素应该是198px,但是实际上会有2px的空白,元素会被填充到200px。

多行文本截断

先上代码

1
2
3
4
5
6
7
.ellipsis {
  width: 200px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

这个没什么好说的了,固定用法,display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;三个属性是结合使用的,缺一不可,其中-webkit-line-clamp: 3;是指定的是行数,根据需要填写。

单行文本截断元素的几个特点也同样应用于此处。另外,尽管box-orient属性已经被废弃,各种标红不建议使用,但事实是并没有一个能够替代的属性出现,且这个被废弃的属性仍然被各大浏览器广泛支持。

使用measureText API进行文本截断

measureText,就按照字面意思理解,用来测量文本,这里还是先上代码,再来解释

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function truncateText(element, width, lineNum = 1) {
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");
  context.font = window.getComputedStyle(element).font;

  // Why trim? see: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
  // 某些情况下,可能需要将前后被trim的空白字符还原
  const text = element.textContent.trim();

  if (context.measureText(text).width <= width) return;

  const ellipsis = "\u2026"; // https://www.compart.com/en/unicode/U+2026
  let result = "",
    startIndex = 0;

  for (let i = 0; i < lineNum; i++) {
    let left = startIndex,
      right = text.length - 1;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);

      // 仅为最后一行的文本增加ellipsis符号
      const testText =
        lineNum === i + 1
          ? text.slice(startIndex, mid + 1) + ellipsis
          : text.slice(startIndex, mid + 1);

      if (context.measureText(testText).width <= width) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }

    result += text.slice(startIndex, right + 1);
    startIndex = right + 1;

    if (startIndex > text.length - 1) {
      break;
    } else {
      if (lineNum === i + 1) {
        result += ellipsis;
      }
    }
  }

  element.textContent = result;
  element.style.width = `${width}px`;
}

meatureText的用法很简单,只需要通过canvas的Context2D对象进行调用即可,这里有一个需要注意的是字体,其默认值为10px sans-serif,需要手动修改,可以不必像我一样利用getComputedStyle来获取,直接用字符串16px Helvetica也是一样的。函数接下来的部分就没什么好说的了,用了一个二分法来查找截断位置的索引,最后获取到截断后的文本即可。这个函数只是用来演示这个思路,实际使用的时候还有优化空间。

有几个注意事项,对应前面CSS截断的几个特点:

  1. DOM中的文本就是看到的文本,包括那三个点,如果要做到全选时复制完整文本则需要添加额外的copy事件完成
  2. 要想省略符号不被选中,可以结合伪元素做到
  3. 其宽度就是实际宽度,要想做到和CSS一样的空白补齐就设置元素的width样式属性

最后,我将上述代码整合到了codepen中,你可以点开自己动手玩一玩

See the Pen 文本截断 by eyebrowkang (@eyebrowkang) on CodePen.

总结

本篇文章梳理了纯CSS的单行和多行文本截断,然后讲解了我使用javascript完成类似效果的一种思路,后续如果遇到其他文本截断的方法我也会补充进来。