朴实的文本截断

文本截断

视觉效果添加...后缀

文本截断,遇到的场景有单行和多行,一般还是单行居多。

处理的方法,也分JavaScrip和CSS两类。

我们来细细看下👀

JavaScript方法

处理单行

JavaScript方法处理单行文本溢出,是最最基本的。

它首先是根据单行宽度,设定一个 标准字长 ,和 我们目标的文本字长做比较,判断文本有没有溢出。操作的是str#length

优势在于简便、无兼容问题,劣势在于对于字母、数字、中文等的视觉效果有不一致。

粗糙场景适用。

处理多行

JavaScript方法处理多行文本溢出,也是最近看到的一种方法,demo-jsfiddle-multilines-cut

JavaScript代码:

const getNumInfo = (value) => +value.slice(0, -2)

//截断多行文本
const truncateMultiLinesText = (selectors, rows = 3, fix = -3) => {
  //取目标元素
  const ele = document.querySelector(selectors);

  //取需要信息
  const text = ele.innerText;
  const totalTextLen = text.length;
  const lineWidth = getNumInfo(window.getComputedStyle(ele).width);
  const fontSize = getNumInfo(window.getComputedStyle(ele).fontSize)


  // 计算:单行字数、多行字数
  const strNum = Math.floor(lineWidth / fontSize);
  const totalStrNum = Math.floor(strNum * rows);

  //确定内容
  const lastIndex = totalStrNum - totalTextLen;
  let content = (totalTextLen > totalStrNum) ? text.slice(0, lastIndex + fix).concat('...') : text

  ele.innerHTML = content;
}

核心比较逻辑在这里:用元素的 width 除以该元素环境下的 fontSize ,得到单行字数,操作的还是str#length

// 计算:单行字数、多行字数
const strNum = Math.floor(lineWidth / fontSize);
const totalStrNum = Math.floor(strNum * rows);

因为还是有偏差,我在函数提供了一个fix参数,值是数字,来调整最终展示的字数长度。

这个方法没有兼容性的烦恼,但是在使用的时候有两个前提

  1. 必须拿到包裹文本的DOM元素上的widthfontSize属性;
  2. 当这个元素的width发生变化(比如100%宽的元素在浏览器窗口大小被改变时候),必须去重新调用计算方法才能响应式;

css方法

处理单行

不必犹豫的text-overflow

.text-truncation--single-line {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

简单、兼容性好、响应式截断,相对于JavaScript方法处理单行的优势是:省略号位置完美无缺!

使用时候的前提:作用的元素需要元素有框定的width(特别是使用了span包裹文本)

处理多行

首推还是 line-clamp

比如两行文本截断:

.text-truncation--two-lines {
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

如果使用了css预处理器,比如less,还可以提取到mixin:

.text-truncation--multi-lines(@input) {
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: @input;
  -webkit-box-orient: vertical;
}

使用它!三行文本溢出:

.text-truncation--three-lines {
  .text-truncation--multi-lines(3);
}

使用时候前提还是:作用的元素要有框定的width值。

这方法最大的问题是兼容性:line-clamp。现在普遍支持还可以,但是如果是需要兼容ie10、ie11的朋友就要绕道了。


再来看看第二种方法,有点黑科技。

demo-jsffidle-multi-lines

思想是用到了::before::after伪类,::before内容是 "..." 的后缀,::after是背景色的遮罩(这里用了白色)。

如果文本没有溢出,::after::before在同一个位置,遮罩会把 "..." 后缀给挡住;

如果文本溢出,::before出现在截断文本末尾,有 "..." 后缀的效果,而::after会落在被overflow:hidden的区域,遮罩也就隐藏掉了。

很巧妙,但是在视觉效果上,可能需要微调。

如果没有兼容性的烦恼,还是用前面第一种方法;如果确实需要兼容ie10、ie11,还是推荐这个大于JavaScrip处理多行的方法。

获取添加...后缀的信息

如果只要求 "..."后缀 视觉效果的,到上面👆就结束了。

但有时候我们可以再往前走走。

比如需要鼠标悬浮在截断处理后的文本时,出现一个展示详情的tooltip,或者是溢出的文本在鼠标悬浮时,变成一个链接样式,点击打开弹窗展示详情。

这里的关键点在于,获取文本有没有溢出这个信息

JavaScript处理文本溢出时,无论是单行还是多行,这个信息本身就是在JavaScript中,是容易获取的。如果是CSS方法处理,要想获得这个信息,得使用一些黑科技了。

这里以CSS方法处理单行文本溢出为例,CSS方法处理多行文本溢出可以类推。

css方法处理单行文本,文本是否溢出

方法一:比较目标元素的 scrollWidthoffsetWidth

方法很简单:

function isEllipsisActive(e) {
     return (e.offsetWidth < e.scrollWidth);
}

这里科普一下offsetWidthclientWidthscrollWidth这几个值。

看图说话:

  • offsetWidthoffsetHeight构成的box,包括 content、padding、border和滚动条;
    • 如果元素是display:block;box-sizing:content(且不含滚动条),那么offsetWidth值能计算,等于width+padding(左右)+border(左右);
  • clientWidthclientHeight 构成的box,包括 content 和 padding;
  • scrollWidthscrollHeight 构成的box,包括包括当前隐藏在滚动区域之外的部分

回过头来看比较的函数就容易了:

如果内容过长,scrollWidth 会包含滚动区域外的部分,也就比offsetWidth长,也就需要添加...后缀。

方法二:拷贝比较

大致思路:

  • 拷贝一份目标元素(一般通过事件获取,e.target),设置必要css属性(这一步决定比较是否公正),添加到body元素,获得不受限制下的width
  • 再和目标元素的width比较,宽度一致则没有添加 "..." 后缀,拷贝元素的宽度大则表明添加了 "..." 后缀
  • 记得删除拷贝的元素

这方法也挺巧妙,缺陷是得直接做DOM的操作。

demo-jsfiddle-copy

获取文本是否溢出的后续操作

这里说一说需求是,需要有展示详情的 tooltip。

一般处理的话,是直接给每个文本包一层 tooltip ,用上一节获取到的是否文本溢出的信息 ,去控制tooltip的隐藏与否即可(disabled属性);

如果你想挑战一下,在一个页面仅维护一个 tooltip ,当鼠标移到目标文本时,手动更新 tooltip 的位置和内容。可以参考这一篇:Plain JavaScript tooltip

抽取复用

考虑的是Vue项目下的复用形式

JavaScript处理单行文本

可以简单抽取一个过滤器,为方便使用在全局注册:

Vue.filter('textTruncation', (val, len = 25) => {
  const suffix = val.length <= len ? '' : '...'
  return `${val.substring(0, len)}${suffix}`
})

css处理文本溢出

TextOverflow组件,用css去控制"..."后缀出现的位置,支持处理单行和多行文本溢出。

在文本溢出情况下,鼠标悬浮,tooltip 显示完整内容。(使用了tooltip

<template>
  <el-tooltip v-bind="$attrs" :disabled="!ellipsisActive">
    <div slot="content">
      <slot>{{content}}</slot>
    </div>
    <section :style="styleObject" @mouseenter="handleMouseEnter">{{ content }}</section>
  </el-tooltip>
</template>

<script>
export default {
  name: 'TyTextOverflow',
  inheritAttrs: false,
  props: {
    content: {
      type: String,
      default: ''
    },
    rows: {
      type: Number,
      default: 1
    }
  },
  data() {
    return {
      ellipsisActive: false,
      singleLineStyle: {
        whiteSpace: 'nowrap',
        overflow: 'hidden',
        textOverflow: 'ellipsis'
      },
      multiLineStyle: {
        overflow: 'hidden',
        display: '-webkit-box',
        '-webkit-box-orient': 'vertical'
      }
    }
  },
  computed: {
    isMulti() {
      return this.rows === 1 ? false : true
    },
    styleObject() {
      return this.isMulti
        ? Object.assign({ '-webkit-line-clamp': this.rows }, this.multiLineStyle)
        : Object.assign({}, this.singleLineStyle)
    }
  },
  methods: {
    handleMouseEnter(e) {
      const target = e.currentTarget
      this.ellipsisActive = this.isEllipsisActive(target, this.isMulti)
    },
    isEllipsisActive(target, isMulti) {
      return isMulti
        ? target.offsetHeight < target.scrollHeight
        : target.offsetWidth < target.scrollWidth
    }
  }
}
</script>

参数说明:

  • content:文本内容,String 类型
  • rows:控制文本行数,Number 类型

其他参数和 tooltip 保持一致。

参考链接