朴实的文本截断

488 阅读7分钟

视觉效果添加...后缀

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

处理的方法,也分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方法处理单行文本,文本是否溢出

方法一:比较 scrollWidth 和 offsetWidth

方法很简单:

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,开销有点大。这时候可以考虑用JavaScript维护一个tooltip,在鼠标移到目标文本的时候,更新tooltip的位置和内容。


可以参考这一篇:Plain JavaScript tooltip

抽取复用

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

JavaScript处理单行文本

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

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


也可以抽一个组件,使用者需要的话,得更新span.single-line__content样式:

<template>
  <section class="single-line">
    <el-tooltip :content="content" :placement="placement" :effect="effect" :disabled="disabled">
      <span class="single-line__content">{{ content | textTruncationLocal(contentLength) }}</span>
    </el-tooltip>
  </section>
</template>

<script>
export default {
  name: 'SingleLineTextUseJS',
  filters: {
    textTruncationLocal(val, len) {
      const suffix = val.length <= len ? '' : '...'
      return `${val.substring(0, len)}${suffix}`
    }
  },
  props: {
    content: {
      type: String,
      default:
        '这里是单行文本截断:哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈'
    },
    contentLength: {
      type: Number,
      default: 25
    },
    effect: {
      type: String,
      default: 'light'
    },
    placement: {
      type: String,
      default: 'bottom'
    }
  },
  computed: {
    disabled() {
      return this.content <= this.contentLength
    }
  }
}
</script>

css处理单行文本

这个 TextOverflow 组件用css处理单行文本,且在鼠标移到元素时判断文本是否溢出,抛出事件告知信息:

<template>
  <section class="text-overflow" @mouseenter="ifTextOverflow">{{ content }}</section>
</template>

<script>
export default {
  name: 'TextOverflow',
  props: {
    content: {
      type: String,
      default: ''
    }
  },
  methods: {
    ifTextOverflow(e) {
      const target = e.currentTarget
      const ellipsisActive = this.isEllipsisActive(target)

      this.$emit('if-ellipsis', ellipsisActive, {
        content: this.content,
        target
      })
    },
    isEllipsisActive(target) {
      return target.offsetWidth < target.scrollWidth
    }
  }
}
</script>

<style lang="less">
.text-overflow {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
</style>


这个 UseTextOverflow 组件使用 TextOverflow 组件,JavaScript维护一个tooltip,出现在正确的位置:

  • 这里使用 mouseleave 事件和 TextOverflow 组件使用 mouseenter 事件,因为不需要冒泡
<template>
  <section class="use-box">
    <section @mouseleave="closeTooltip">
      <TextOverflow :content="value1" @if-ellipsis="handleEllipsisActive"></TextOverflow>
    </section>
    <section @mouseleave="closeTooltip">
      <TextOverflow :content="value2" @if-ellipsis="handleEllipsisActive"></TextOverflow>
    </section>

    <section :style="styleObject" class="tooltip__popper--light" @mouseleave="closeTooltip">
      {{ tooltipText }}
    </section>
  </section>
</template>

<script>
import TextOverflow from '../../../resource/TextOverflow'

export default {
  name: 'UseTextOverflow',
  components: {
    TextOverflow
  },
  data() {
    return {
      value1:
        '这里是css单行文本截断:悬浮查看是否文本溢出哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈',
      value2: '这里是css单行文本截断:悬浮查看是否文本溢出',
      tooltipText: '',
      styleObject: {
        display: 'none'
      }
    }
  },
  methods: {
    handleEllipsisActive(ellipsisActive, info) {
      if (!ellipsisActive) return

      this.openTooltip(info)
    },
    //打开tooltip
    openTooltip({ content, target }) {
      this.styleObject.display = 'block'
      this.styleObject.top = `${target.offsetTop + 25}px`
      this.styleObject.left = `${target.offsetLeft - 5}px`
      this.tooltipText = content
    },
    //关闭tooltip
    closeTooltip(e) {
      if (this.ifToTooltip(e.toElement)) return
      this.styleObject.display = 'none'
      this.tooltipText = ''
    },
    ifToTooltip(element) {
      return element.className === 'tooltip__popper--light'
    }
  }
}
</script>

<style lang="less">
.use-box {
  width: 400px;
  border: 1px solid black;
  .tooltip__popper--light {
    position: absolute;
    border-radius: 4px;
    padding: 10px;
    z-index: 2000;
    font-size: 12px;
    line-height: 1.2;
    min-width: 10px;
    word-wrap: break-word;

    background: #fff;
    border: 1px solid #303133;

    &::before {
      content: '';
      position: absolute;
      top: -16px;
      left: calc(~'50% - 6px');
      border: 8px solid transparent;
      border-bottom-color: #303133;
    }
    &::after {
      content: '';
      position: absolute;
      top: -17px;
      left: calc(~'50% - 8px');
      border: 10px solid transparent;
      border-bottom-color: #fff;
    }
  }
}
</style>

未完待研究

  • [ ] 处理多行文本情况,如何取添加...后缀的信息,如何抽取复用
  • [ ] clientWidthscrollWidthoffsetWidth和滚动条的关系

参考链接