多行文本超过指定行数隐藏超出部分并显示“...查看全部”是一个常遇到的需求,网上也有人实现过类似的功能,不过还是想自己写写看,

于是就写了一个Vue的组件,本文简单介绍一下实现思路。

遇到这个需求的同学可以尝试一下这个组件,支持npm安装使用:

一、需求描述

长度不定的一段文字,最多显示n行(比如3行),不超过n行正常显示;超过n行则在最后一行尾部显示“展开”或“查看全部”之类的按钮,点击按钮则展开显示全部内容,或者跳转到其它页面展示所有内容。

预期效果如下:

二、实现原理

纯CSS很难完美实现这个功能,所以还得借助JS来实现,实现思路大体相似,都是判断内容是否超过指定行数,超过则截取字符串的前x个字符,然后然后和“...查看全部”拼接在一起,这里的x即截取长度,需要动态计算。

想通过上述方案实现,有几个问题需要解决:

  • 怎样判断文字是否超过指定行数
  • 如何计算字符串截取长度
  • 动态响应,包括响应页面布局变动、字符串变化、指定行数变化等
  • 下面具体研究一下这些问题。

1. 怎样判断一段文字是否超过指定行数?

首先解决一个小问题:如何计算指定行数的高度?我首先想到的是使用textarea的rows属性,指定行数,然后计算textarea撑起的高度。另一个方法是将行高的计算值与行数相乘,即得到指定行数的高度,这个办法我没尝试过,但是想必可行。

解决了指定行数高度的问题,计算一段文字是否超过指定行数就很容易了。

我们可以将指定行数的textarea使用绝对定位absolute脱离文档流,放到文字的下方,然后通过文本容器的底部与textarea的底部相比较,如果文本容器的底部更靠下,说明超过指定行数。

这个判断可以通过getBoundingClientRect接口获取到两个容器的位置、大小信息,然后比较位置信息中的bottom属性即可。

可以这样设计DOM结构:

 <div class="ellipsis-container">    <div class="textarea-container">      <textarea rows="3" readonly tabindex="-1"/>    div>   <-- showContent表示字符串截取部分 -->     {{ showContent }}     ... 查看更多div>

然后使用CSS控制textarea,使其脱离文档流并且不能被看到以及被触发鼠标事件等(textarea标签中的readonly以及tabIndex属性是必要的):

.ellipsis-container {  text-align: left;  position: relative;  line-height: 1.5;  padding: 0 !important;}

.textarea-container {  position: absolute;  left: 0;  right: 0;  pointer-events: none;  opacity: 0;  z-index: -1;}

textarea {  vertical-align: middle;  padding: 0;  resize: none;  overflow: hidden;  font-size: inherit;  line-height: inherit;  outline: one;  border: none;}

2.如何计算字符串截取长度x——双边逼近法(二分思想)

只要可以判断一段文字是否超过指定行数,那我们就可以动态地尝试截取字符串,直到找到合适的截断长度x。

这个长度满足从x的位置截断字符串,前半部分+“...查看全部”等文字刚好不会超出指定行数N,但是多截取一个字,则会超出N行。

最直观的想法就是直接遍历,让x从0开始增长到显示文本总长度,对于每个x值,都计算一次文字是否超过N行,没超过则加继续遍历,超过则获得了合适的长度x - 1,跳出循环。当然也可以让x从文本总长度递减遍历。

不过这里最大的问题在于浏览器的回流和重绘。因为我们每次截取字符串都需要浏览器重新渲染出来才能得到是否超过N行,这过程中就触发了浏览器的重绘或回流,每次循环都会触发一次。

而对于正常的需求来说,假设N取值是3,那很可能每次计算会导致50次以上的重绘或回流,这中间消耗的性能还是非常大的,不小心可能就是几十毫秒甚至上百毫秒。这个计算过程应该在一个任务(即常说的”宏任务“)中完成,否则计算过程中会出现显示闪动的”异常“情况,所以可以说计算过程是阻塞的,因此计算的总时间一定要控制到非常低,即要减少计算的次数。

可以考虑使用"双边逼近法"(或称”二分法“)查找合适的截取长度x,大大减少尝试的次数。

第一次先以文本长度为截取长度,计算是否超过N行,没超过则停止计算;超过则取1/2长度进行截取,如果此时没超过N行,则在1/2长度到文本长度之间继续二分查找,如果超过则在0到1/2文本长度中继续二分查找。

直到查找区间开始值与结束值相差为1,则开始值即为所求。具体实现可以看下文中的完整代码。

3.监听页面变动

对于Vue项目来说,传入组件的字符串、行数等可能随时改变,可以watch这些属性变化,然后重新计算一次截取长度。

另一方面,对于页面布局而言,可能会因为其它页面元素的增删或者样式改变,导致页面布局变动,影响到文本容器的宽度,此时也应该重新计算一次截取长度。

监听文本容器宽度的变化,可以考虑使用ResizeObserver来监听,但是这个接口的兼容性不够好(IE各个版本都不支持),因此选择了一个npmelement-resize-detector来监测(非常好用?)。

三、代码实现

<template>  <div class="ellipsis-container">    <div class="textarea-container" ref="shadow">      <textarea :rows="rows" readonly tabindex="-1">textarea>    div>    {{ showContent }}    <slot name="ellipsis" v-if="(textLength >      {{ ellipsisText }}<span class="ellipsis-btn" @click="clickBtn">{{ btnText }}span>slot>div>template>
import resizeObserver from 'element-resize-detector'const observer = resizeObserver()

export default {  props: {    content: {      type: String,      default: ''    },    btnText: {      type: String,      default: '展开'    },    ellipsisText: {      type: String,      default: '...'    },    rows: {      type: Number,      default: 6    },    btnShow: {      type: Boolean,      default: false    },  },  data () {    return {      textLength: 0,      beforeRefresh: null    }  },  computed: {    showContent () {      const length = this.beforeRefresh ? this.content.length : this.textLength      return this.content.substr(0, this.textLength)    },    watchData () { // 用一个计算属性来统一观察需要关注的属性变化      return [this.content, this.btnText, this.ellipsisText, this.rows, this.btnShow]    }  },  watch: {    watchData: {      immediate: true,      handler () {        this.refresh()      }    },  },  mounted () {    // 监听尺寸变化    observer.listenTo(this.$refs.shadow, () => this.refresh())  },  beforeDestroy () {    observer.uninstall(this.$refs.shadow)  },  methods: {    refresh () { // 计算截取长度,存储于textLength中      this.beforeRefresh && this.beforeRefresh()      let stopLoop = false      this.beforeRefresh = () => stopLoop = true      this.textLength = this.content.length      const checkLoop = (start, end) => {        if (stopLoop || start + 1 >= end) return        const rect = this.$el.getBoundingClientRect()        const shadowRect = this.$refs.shadow.getBoundingClientRect()        const overflow = rect.bottom > shadowRect.bottom        overflow ? (end = this.textLength) : (start = this.textLength)        this.textLength = Math.floor((start + end) / 2)        this.$nextTick(() => checkLoop(start, end))      }      this.$nextTick(() => checkLoop(0, this.textLength))    },    // 展开按钮点击事件向外部emit    clickBtn (event) {      this.$emit('click-btn', event)    },  }}

在代码实现中refresh函数用于计算截取长度,在文本内容、rows属性等发生改变或者文本容器尺寸改变时将被调用。

每次refresh调用会异步地递归调用多次checkLoop,refresh可能重新调用,新的refresh调用将结束之前的checkLoop的调用。

四、其它

1. 支持HTML串的考虑

现在的实现方案并不支持内容是HTML文本,如果需要支持HTML文本,问题将复杂许多。主要在于HTML字符串的解析和截断,不像文本字字符串那么简单。

不过或许可以借助浏览器的Range API 来实现截断位置的定位,RangeinsertNode以及setStart接口可以将“...查看全部”插入到指定位置,而如果插入位置刚好符合需要,则可以通过Range.cloneContents()接口取得截取HTML字符串的相关内容,理论上是可行的,不过具体细节以及处理效率得实践后才知道。

2. 减少浏览器回流的影响

上述实现方案中,每一次截取都需要浏览器重新渲染DOM,即重绘。

重绘的影响还比较小,而如果截取的字符串行数发生改变,还会引发文本容器的高度变化,这时候就会导致浏览器回流,而文本容器在文档流中,回流将会影响整个文档。

想解决这个问题,可以使用一个脱离文档流的元素来进行字符串动态截断后的渲染与判断,布局就类似上述的textarea

因为不在文档流中,回流的影响范围就会减少到该元素自身。获得截断长度后再截断文本,渲染到真正的文本容器即可。

本文仅作为一个简单的原理概述的示例,没有做这个处理,对具体细节感兴趣的同学,可以查看github仓库代码。

组件地址:

https://github.com/Lushenggang/vue-overflow-ellipsis

在线体验:

https://wintc.top/laboratory/#/ellipsis

文章:

https://wintc.top/article/58

1.如果看到这里,说明你喜欢这篇文章,请 转发点赞、在看

2.关注公众号前端人,回复资料包领取我整理的前端进阶资料包

3.回复加群,加入前端进阶群,和小伙伴一起学习讨论!

overflow超出显示_实现:超过N行折叠并显示“...查看全部”【功能】相关推荐

  1. 多行文本超过一定行数后显示展开功能

    超过单行显示三个点 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; 超过多行显示三个点 display: -webkit ...

  2. firebase 发生消息_如何在命令行提示符下显示当前的Firebase项目名称,以防止发生危险错误...

    firebase 发生消息 by Thang Minh Vu Thang Minh Vu 如何在命令行提示符下显示当前的Firebase项目名称,以防止发生危险错误 (How to show your ...

  3. android怎么截取接口返回html代码中的内容_如何实现文本内容折叠并显示“...查看全部”?...

    来源 | https://wintc.top/article/58多行文本超过指定行数隐藏超出部分并显示"...查看全部"是一个常遇到的需求,网上也有人实现过类似的功能,不过还是想 ...

  4. 华为手机如何调时间显示_华为手机不亮屏也能显示时间日期?10秒就能设置,原来这么简单...

    不知道各位在想要查看时间的时候,是不是从口袋中拿出手机,然后按下锁屏键亮屏查看时间之后再按锁屏键熄屏的呢?但是还有一种更加简单的方法不需要亮屏就可以查看时间日期. 这个功能就是华为手机的"灭 ...

  5. html5文本超过指定行数隐藏显示省略号

    这个很简单,直接贴代码就好了 HTML <span class="name">博客园是一个面向开发者的知识分享社区.自创建以来,博客园一直致力并专注于为开发者打造一个纯 ...

  6. 的run窗口不显示_「玩转deepin」如何安装VirtualBox增强功能使得deepin全屏显示?...

    在Windows上通过Virtualbox安装deepin深度操作系统,安装完成后,为了让deepin可以在virtualbox中全屏显示,需要安装增强工具,但是不少同学点击安装增强工具后会出现无法安 ...

  7. java byte 图片浏览器直接显示_在imge控件中直接显示图片(图片是byte[]格式)

    在工作过程中遇到了这个问题,在网上查了一些资料,结合自己的解决方法及解决过程总结了下,方面以后查阅.如果能帮到同样遇到这个问题的你,将非常高兴哦~_~ 由于asp.net中的Image控件是在Syst ...

  8. 门店定位怎么在地图上显示_怎样让自己的店地理位置显示在高德地图上

    展开全部 高德地图软件中进行新增地点提交,数据审核32313133353236313431303231363533e78988e69d8331333365666163后会尽快在地图上标注.目前不是所有 ...

  9. 华为手机如何调时间显示_华为手机照相有时间日期显示怎样设置

    华为手机照相有时间日期显示设置方法如下: 1.在手机界面找到图库点击打开. 2.进入图库后选择一张照片打开. 3.打开图片后.点击下图所示右上角的选项. 4.进入后会看到显示拍摄时间和拍摄地点没有打开 ...

最新文章

  1. android默认exported_android:exported 属性详解
  2. 【C】——如何用线程进行参数的传递
  3. Angular vs React 最全面深入对比
  4. Metrics.NET 项目
  5. 在SAP Hybris commerce Storefront里购物下单
  6. ubuntu13.10无法登陆
  7. 雷神开机logo更改_黑武士再度来袭 雷神第三代911黑武士游戏台式机评测
  8. 代码刚提交暂存区,组长突然要我把新增代码 Commit另一分支怎么办?
  9. bzoj 1640 bzoj 1692: [Usaco2007 Dec]队列变换(后缀数组)
  10. Menubutton按钮弹出菜单
  11. Python 数据可视化之matpotlib画图
  12. 【优化分类】基于matlab遗传算法优化支持向量机分类(多输入多分类)【含Matlab源码 QF003期】
  13. AC自动机1030 [JSOI2007]文本生成器
  14. 10分钟搭建一个H5商城,支持微信支付和各平台小程序
  15. python如何截长图_python如何实现对元素的长截图功能 python实现对元素的长截图功能实例...
  16. color 常用色值
  17. cccc2016决赛9
  18. Spark学习(6)-Spark SQL
  19. What is tethering and how do you enable tethering?
  20. Ubuntu系统下打开chm文件

热门文章

  1. GC之7大垃圾收集器详解(下)
  2. 直播马上开始|不要怂,一起上!关于黑客攻防,我们有话要说
  3. OpenStack基金会携手Intel、Hyper发布开源Kata Containers项目
  4. lua实现多继承-方式2
  5. leetcode 413. Arithmetic Slices | 413. 等差数列划分(Java)
  6. leetcode 318. Maximum Product of Word Lengths | 318. 最大单词长度乘积
  7. 面试必会系列 - 1.1 Java SE 基础
  8. html不可选择的按钮,HTML功能无法使用按钮
  9. java幂等性的控制(技术论坛上整理成文)
  10. 使用Introspector(Java内省机制)实现Map转换为JavaBean