关于前端文本溢出省略那些事

483 阅读8分钟

前言

文本溢出省略是前端开发过程当中经常会碰到的一个场景,本文将从css文本溢出省略入手,从其实现到状态判定以及改进方式的讲解。

什么是文本溢出省略?

在日常开发中,我们会碰到这种场景,在一个固定宽度和高度的容器中,有一段文本。这段文本的长度未知,所以可能会出现超出容器范围的情况。这个时候,我们就希望在它超出容器范围的情况下,文本溢出部分变成省略号。

image.png

css文本溢出省略的实现

单行文本溢出省略:

核心:

实现单行文本溢出省略,我们需要用到以下几个属性:

  • overflow:hidden:实现超出容器的部分隐藏。
  • text-overflow: ellipsis: text-overflow用于控制文本隐藏时的显示方式,有两种属性clip(默认属性)和ellipsis,作用分别是截断和省略。
  • white-space: nowrap:禁止文本换行。

优点:

  • 无兼容问题
  • 响应式截断
  • 文本溢出范围才显示省略好,否则不显示省略号
  • 省略号位置显示刚好

缺点:

  • 只支持单行文本

Demo:

<style>
    .box{
        width: 200px;
        border: 1px solid red;
        margin-top: 10px;
    }
    .ellipsis-oneline{
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
</style>
<div>
    <div class="box">
        test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
    </div>
    <div class="box ellipsis-oneline">
        test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
    </div>
</div>

效果:

image.png

多行文本溢出省略:

核心

实现多行文本溢出省略,我们需要用到下面几个属性:

  • display: -webkit-box 是一个早期的、WebKit浏览器特有的属性值,用于创建一个伸缩容器(flexbox)。这个属性是早期的flexbox实现,并且主要在WebKit浏览器(如早期的Chrome和Safari版本)中得到支持。
  • -webkit-line-clamp 是一个非标准的CSS属性,主要用于WebKit浏览器引擎(如Chrome、Safari和一些旧版本的Edge浏览器)。这个属性的作用是限制块级元素(如<div><p>)中显示的文本行数。
  • -webkit-box-orient 是一个早期的、WebKit浏览器特有的CSS属性,用于设置伸缩容器(flexbox)中子元素的排列方向。这个属性是WebKit浏览器对flexbox布局的早期实现的一部分。

以上三个属性需要结合使用。

  • overflow以及text-overflow 作用同单行溢出省略

优点:

  • 响应式截断
  • 文本溢出范围才显示省略号,否则不显示省略号
  • 省略号显示位置刚好

缺点:

  • 兼容性差,只支持webkit内核的浏览器(现代浏览器基本都得到了很好的支持)

Demo:

<body>
    <style>
        .box{
            width: 193px;
            border: 1px solid red;
            margin-top: 10px;
        }
        .ellipsis-threeline{
            display: -webkit-box;
            overflow: hidden;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
            text-overflow: ellipsis;
        }
    </style>
    <div>
        <div class="box">
            test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
        </div>
        <div class="box ellipsis-threeline">
            test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
        </div>
    </div>
</body>

效果:

image.png

用js判断是否文本溢出省略

法一:克隆元素

原理:

克隆一个一样的dom,挂载在body上、绝对定位且透明化,取消它的溢出限制,然后和原dom进行高度比较,如果比原来高,就说明文本溢出。

优点:

能够很好的兼容各种文本溢出实现方式的判断。

缺点:

每次比较都需要创建dom元素,然后删除dom元素,效率不高。

Demo:

<body>
    <style>
        .box{
            width: 200px;
            border: 1px solid red;
            margin-top: 10px;
        }
        .ellipsis-threeline{
            display: -webkit-box;
            overflow: hidden;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
            text-overflow: ellipsis;
        }
    </style>
    <div>
        <div class="box ellipsis-threeline" id="myMultilineText-1">
            test test test test test test test
        </div>
        <div class="box ellipsis-threeline" id="myMultilineText-2">
            test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
        </div>
    </div>
    <script>
        function isMultilineTextEllipsis(elementId) {  
            const originalElement = document.getElementById(elementId);  
            if (!originalElement) return false;  
            // 创建克隆元素并移除样式限制  
            const clone = originalElement.cloneNode(true);  
            clone.style.display = '-webkit-box'; // 确保克隆元素仍然使用box布局  
            clone.style.WebkitBoxOrient = 'vertical';  
            clone.style.overflow = 'visible'; // 移除溢出隐藏  
            clone.style.textOverflow = 'clip'; // 移除文本省略样式  
            clone.style.whiteSpace = 'normal'; // 确保文本可以正常换行  
            clone.style.webkitLineClamp = 'unset'; // 移除行数限制  
            clone.style.visibility = 'hidden'; // 隐藏克隆元素以防止它在页面上可见  
            clone.style.position = 'absolute'; // 防止克隆元素干扰页面布局  
            // 将克隆元素添加到DOM中(通常添加到原始元素的父元素或body中)  
            document.body.appendChild(clone);  
            // 比较高度  
            const isEllipsis = clone.offsetHeight > originalElement.offsetHeight;
            console.log('cloneHeight:' + clone.offsetHeight,'originHeight:' + originalElement.offsetHeight);
            // 移除克隆元素  
            document.body.removeChild(clone);  
            return isEllipsis;  
        }  
        // 使用函数  
        console.log(`文本1:${isMultilineTextEllipsis('myMultilineText-1') ? '被省略了': '没有被省略'}`);  
        console.log(`文本2:${isMultilineTextEllipsis('myMultilineText-2') ? '被省略了': '没有被省略'}`);  
    </script>
</body>

效果:

image.png

法二:比较scrollWidth与clientWidth

原理:

当文本处于单行缩略状态时,其scrollWidth并不会固定,而是保持正常未缩略状态下的宽度。所以可以利用这一点进行比较。

优点:

实现简单,性能好。

缺点:

不支持多行省略,只支持css单行省略方式的判断。

Demo:

<body>
    <style>
        .box{
            width: 200px;
            border: 1px solid red;
            margin-top: 10px;
        }
        .ellipsis-oneline{
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .ellipsis-threeline{
            display: -webkit-box;
            overflow: hidden;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
            text-overflow: ellipsis;
        }
    </style>
    <div>
        <div class="box ellipsis-threeline" id="myMultilineText-1">
            test test test test test test test
        </div>
        <div class="box ellipsis-oneline" id="myMultilineText-2">
            test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
        </div>
        <div class="box ellipsis-threeline" id="myMultilineText-3">
            test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test
        </div>
    </div>
    <script>
        const isEllipsis = (elementId) => {
            const dom = document.getElementById(elementId);
            return dom.scrollWidth > dom.clientWidth;
        }
        console.log('myMultilineText-1 isEllipsis:', isEllipsis('myMultilineText-1'));
        console.log('myMultilineText-2 isEllipsis:', isEllipsis('myMultilineText-2'));
        console.log('myMultilineText-3 isEllipsis:', isEllipsis('myMultilineText-3'));
    </script>
</body>

效果:

image.png

CSS文本溢出省略的缺陷以及改进

前面我们都是通过纯css的方式实现文本省略,但是这样实现的文本溢出省略存在一些缺陷,没有办法支持更复杂的场景。

那就是不支持对省略符号的修改以及对附加操作按钮的支持(比如置于最后的 复制、编辑、展开 等按钮)。

image.png

操作按钮会被一同截断,无法显示。

image.png

虽然有一些黑魔法手段可以通过诸如 float 样式来实现,但是这样的方式在不同的浏览器中需要做针对性处理。此外仍然无法解决自定义省略符号的问题。因而目前最好的实现方式仍然是通过 JS 来实现。

js实现文本溢出

原理:

根据文本的长度、文字的大小、容器的宽度以及最大行数来计算是否需要省略。

Demo:

<body>
    <style>
        .box{
            width: 200px;
            border: 1px solid red;
            margin-top: 10px;
            font-size: 16px;
        }
    </style>
    <div>
        <div class="box" id="myMultilineText-1">
            这是一段很长的文本这是一段很长的文本
        </div>
        <div class="box" id="myMultilineText-2">
            这是一段很长的文本这是一段很长的文本
        </div>
    </div>
    <script>
        function formatStr(eleId, lineNum){
            const ele = document.getElementById(eleId);
            let text = ele.innerText;
            if(!ele) return;
            const baseWidth = window.getComputedStyle(ele).width;
            const baseFontSize = window.getComputedStyle(ele).fontSize;
            const lineWidth =+ baseWidth.slice(0, -2);
            const totalTextLen = text.length;
            // 所计算的strNum为元素内部一行可容纳的字数(不区分中英文)
            const strNum = Math.floor(lineWidth / +baseFontSize.slice(0, -2));
            let content = '';
            // 多行可容纳总字数
            const totalStrNum = Math.floor(strNum * lineNum);
            const lastIndex = totalStrNum - totalTextLen;
            console.log(`一共${lineNum}行,是否缩略:${totalTextLen > totalStrNum}`);
            if (totalTextLen > totalStrNum) {
                content = text.slice(0, lastIndex - 1).concat('...');
            } else {
                content = text;
            }
            ele.innerHTML = content;
        }
        formatStr('myMultilineText-1', 1);
        formatStr('myMultilineText-2', 2);
    </script>
</body>

效果:

image.png

进阶+能力封装

由这个原理我们可以进一步扩展,实现自定义尾部以及展开/复制按钮的实现。并且做成一个组件,方便复用。并且做成一个组件,方便复用。

Demo:

Ellipsis组件:
import React, { useEffect, useRef, useState } from "react";
import './index.less';

function copyToClipboard(text: string) {  
    navigator.clipboard.writeText(text).then(function() {  
        console.log('复制成功!');  
    }).catch(function(err) {  
        console.error('无法复制', err);  
    });  
} 

export const Ellipsis: React.FC<EllipsisProps> = (props) => {
    const containerRef = useRef(null);
    const expandableBtnRef = useRef(null);
    const copyableBtnRef = useRef(null);
    const {
      ellipsis = false,
      rows = 0,
      text = '',
      suffix = '',
      expandable = false,
      copyable = false,
      onEllipsis,
    } = props;
    const [content, setContent] = useState(text);
    const [status, setStatus] = useState(true);
    const textLen = text.length; // 文本长度
    const suffixLen = suffix.length; // 后缀长度
    const ellipsisContent = '...'; // 省略号
    const ellipsisLen = 1; // 省略号长度
    const expandableBtn = <span className="expandableBtn" ref={expandableBtnRef} onClick={() => {
        setStatus(false);
    }}>
        展开
    </span>
    const copyableBtn = <span className="copyableBtn" ref={copyableBtnRef} onClick={() => {
        copyToClipboard(text);
    }}>
        复制
    </span>
    useEffect(() => {
        if(ellipsis && containerRef.current){
            const containerComputedStyle = window.getComputedStyle(containerRef.current);
            // 容器宽度
            const Width = +containerComputedStyle.getPropertyValue('width').slice(0, -2);
            // 字体大小
            const FontSize = +containerComputedStyle.getPropertyValue('font-size').slice(0, -2);
            // 文本长度
            //  单行容纳文本数
            const onelineStrNum = Math.floor(Width / FontSize);
            // 展开按钮占几位文本
            let expStrNum = 0;
            if(expandable && expandableBtnRef.current){
                const expandableComputedStyle = window.getComputedStyle(expandableBtnRef.current);
                // 复制的宽度
                const expandableWidth = +expandableComputedStyle.getPropertyValue('width').slice(0, -2);
                expStrNum = Math.floor(expandableWidth / FontSize);
            }
            // 复制按钮占几位文本
            let copyStrNum = 0;
            if(copyable && copyableBtnRef.current){
                const copyableComputedStyle = window.getComputedStyle(copyableBtnRef.current);
                // 复制的宽度
                const copyableBtnWidth = +copyableComputedStyle.getPropertyValue('width').slice(0, -2);
                copyStrNum = Math.floor(copyableBtnWidth / FontSize);
            }
            const totalStrNum = onelineStrNum * rows;
            const lastIndex = totalStrNum - textLen - expStrNum - copyStrNum;
            if (textLen + suffixLen > totalStrNum) {
                setStatus(true);
                setContent(text.slice(0, lastIndex - ellipsisLen - suffixLen).concat(ellipsisContent));
            } 
        }
    }, []);
    // 监听ellipsis的变化
    useEffect(() => {
        if(!status)
        setContent(text);
        if(onEllipsis)
        onEllipsis(status);
    }, [status]);
    return <div className="container" ref={containerRef}>
        {content}{suffix}{copyable && copyableBtn}{status && expandable && expandableBtn}
    </div>
}
  
interface EllipsisProps {
    ellipsis?: boolean; // 是否开启缩略
    rows?: number; // 总行数
    expandable?: boolean; // 展开按钮
    copyable?: boolean; // 复制按钮
    suffix?: string, // 省略后缀
    onEllipsis?: (ellipsis: boolean) => void; // 省略状态变化的回调
    text?: string;
}
copyToClipboard('这是要复制的文本');
  
使用:
<div className='box'>
  <Ellipsis 
    ellipsis 
    rows={2} 
    suffix='——后缀' 
    expandable 
    text="这是一段很长的文本这是一段很长的文本这是一段很长的文本"
    onEllipsis={(status) => {
      console.log(status);
    }}
  />
</div>
<div className='box'>
  <Ellipsis 
    ellipsis 
    rows={2} 
    copyable
    text="这是一段很长的文本这是一段很长的文本这是一段很长的文本"
    onEllipsis={(status) => {
      console.log(status);
    }}
  />
</div>
效果:

image.png

总结:

文本溢出以及溢出状态获取方式优点局限
单行css省略+scrollWidth判断法实现最简单/无兼容问题只支持单行省略,不支持更复杂功能的扩展
多行css省略+元素克隆法实现较为简单有兼容问题,不支持更复杂功能的扩展
js计算法兼容性强、可以扩展出各种强大功能实现复杂,但是封装成组件以后可以高效复用

除了上述的几种方式外,其实还可以通过float属性又或者是别的js计算方式实现文本省略功能。

继续扩展的话,其实也还有诸如文本中间省略、中间穿插图片等更复杂的功能,不过总体来讲大同小异,都可以通过js计算实现。