再谈文字省略符的实现

avatar
豌豆公主会员/攥着玻璃珠的小P孩 @豌豆公主

1. 一档:单行省略

如果某个区域文字太多而显示区域有限,我们就会选择只显示部分文字,然后最后显示3个点的省略号。实现的方案很简单,前端新同学都可以轻松实现,就是用到了text-overflow: ellipsis;这个属性。

参考:

2. 二档:多行省略

当然,业务中还有多行省略的需求,

image.png

比如商品卡片中商品名的显示,要求最多显示两行,超过的内容在第二行结束时截断,显示省略号。

多行省略的方案就很多了,最简单的使用css trick:用旧版的flex布局-webkit-box,配合css属性-webkit-line-clamp,可以方便实现多行的省略。demo:

p {
  width: 300px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

还有很多其它方案,比如把省略符做为单独一层盖在内容上,模拟省略效果的;配合js获得字符位置的,甚至使用canvas来渲染的。

参考:

3. 三档:多行省略且省略符不在最后

image.png 如图,多行省略,且右下角不是省略符,而是一个链接,链接左边才是截断后显示的省略号。

本人不才,目前还没找到纯css的方案可以优雅的解决这个问题。我使用了Range这个对象,配合js,暂时为相对优雅的方式解决。dom简单,需要的css不多,js量也少,容易封装。

首先dom部分:

<div class="ellipsis">
    <span class="text">豌豆公主是目前唯一由日本供应商直接进驻的跨境电商平台,独家代理日本高端品牌。</span>
    <div class="link">
        <span>... </span>
        <a>查看详情&gt;</a>
    </div>
</div>

其中.text中就是我们要显示和截断的内容,.link中是右下角的链接。注意在链接前有一个省略符,一会儿会讲它的用处。

然后css来实现简单布局

.ellipsis{
    position: relative;
    width: 200px;
    height: 40px;
    border: 1px solid #000;
    line-height: 20px;
    overflow: hidden;
    font-size: 14px;
}

.link{
    position: absolute;
    right: 0;
    bottom: 0;
    color: red;
}
.link span{
    opacity: 0;
}

目前的效果如下

image.png

链接已经放到了右下角,并且超过两行的内容也被截掉了。不过链接和第二行文本有重叠,第二行文本还需要向左继续截断。

注意,从css上可以看出,和链接放在一起的省略号,透明度为0,是显示不出来的

接下来我的思路时:

  1. 获得.link的位置,可以使用element.getBoundingClientRect()轻松获得,记为linkRect
  2. 然后检测.text中的每一个文字的位置,记为letterRect。如果letterRect和linkRect有交集,如果有交集,就说明要在此文字前截断;然后在截图后的文本后补上省略号就行。因为在link中考虑到了省略号占用的空间,所以最终结果上可以自信的补上省略号。

那如何获得每个字符的位置呢?这就可以用到Range这个对象了。按MDN定义:The Range interface represents a fragment of a document that can contain nodes and parts of text nodes。它表示包含节点或文本节点的文档片断,在web富文本编辑器中,经常会用到它。

<div id="desc">这里是一段demo文本</div>
<script>
    var textNode = document.querySelector('#desc').childNodes[0];
    var range = document.createRange();
    range.setStart(textNode, 0);
    range.setEnd(textNode, 3);
</script>

上面的range就是.desc中这里是三个词的文本节点片断。

同时,range有一个很有意思的特性:它可以折叠。如上面例子,如果设置结束和开始一样,那它就折叠了。

    range.setStart(textNode, 1);
    range.setEnd(textNode, 1);

如上,此时range折叠,它表示两个字符中间的位置。

另外,Range本身也有getBoundingClientRect方法,获得此文档片断的位置。结果Range的折叠功能和位置能力,我们就可以实现上面的思路了。

// content为要截断的文本元素
const content = document.querySelector('.ellipsis span');
// 它的第一个节点就是文本节点,可以传给range对象的
const textNode = content.childNodes[0];
const range = document.createRange();
// 获得链接元素的位置。注意它里面是包含了省略号的空间了。
const linkRect = document.querySelector('.link').getBoundingClientRect();
const text = content.innerText;
let max = 0;
for(let i=0;i<text.length;i++){
    // 把range折叠到某个文字前面
    range.setStart(textNode, i);
    range.setEnd(textNode, i);
    // 获得折叠的位置,即这个字符前这个位置
    const rect = range.getBoundingClientRect();
    // 如果和link的空间有交叉,说明需要从上一个字符前截断
    if(rect.bottom > linkRect.top && rect.left>linkRect.left) {
        max = i-1;
        break;
    }
}
if(max>0) {
    textNode.textContent = text.substring(0, max)+'...';
}

上面方案的优点:

  1. dom的代码和结构很简单,不需要特殊的dom结构和辅助的元素;
  2. 样式简单,只需要简单控制超出内容隐藏就行了,不需要很奇巧淫技的css属性;
  3. js最后只对dom做了一些内容调整,并且未改变区域高度,不会引起页面大面积重绘,性能很好 最终实现demo,查看>>

参考