本文发布于我的个人网站:https://wintc.top/article/58,转载请注明。
多行文本超过指定行数隐藏超出部分并显示“...查看全部”是一个常遇到的需求,网上也有人实现过类似的功能,不过还是想自己写写看,于是就写了一个vue的组件,本文简单介绍一下实现思路。
遇到这个需求的同学可以尝试一下这个组件,支持npm安装使用:
组件地址:https://github.com/Lushenggang/vue-overflow-ellipsis
在线体验:https://wintc.top/laboratory/#/ellipsis
长度不定的一段文字,最多显示n行(比如3行),不超过n行正常显示;超过n行则在最后一行尾部显示“展开”或“查看全部”之类的按钮,点击按钮则展开显示全部内容,或者跳转到其它页面展示所有内容。
预期效果如下:
纯css很难完美实现这个功能,所以还得借助JS来实现,实现思路大体相似,都是判断内容是否超过指定行数,超过则截取字符串的前x个字符,然后然后和“...查看全部”拼接在一起,这里的x即截取长度,需要动态计算。
想通过上述方案实现,有几个问题需要解决:
怎样判断文字是否超过指定行数
如何计算字符串截取长度
动态响应,包括响应页面布局变动、字符串变化、指定行数变化等
下面具体研究一下这些问题。
首先解决一个小问题:如何计算指定行数的高度?我首先想到的是使用textarea的rows属性,指定行数,然后计算textarea撑起的高度。另一个方法是将行高的计算值与行数相乘,即得到指定行数的高度,这个办法我没尝试过,但是想必可行。
解决了指定行数高度的问题,计算一段文字是否超过指定行数就很容易了。我们可以将指定行数的textarea使用绝对定位absolute脱离文档流,放到文字的下方,然后通过文本容器的底部与textarea的底部相比较,如果文本容器的底部更靠下,说明超过指定行数。这个判断可以通过getBoundingClientRect接口获取到两个容器的位置、大小信息,然后比较位置信息中的bottom属性即可。
可以这样设计dom结构:
<div class="ellipsis-container">
<div class="textarea-container">
<textarea rows="3" readonly tabindex="-1"></textarea>
</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 none
border none
只要可以判断一段文字是否超过指定行数,那我们就可以动态地尝试截取字符串,直到找到合适的截断长度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,则开始值即为所求。具体实现可以看下文中的完整代码。
对于Vue项目来说,传入组件的字符串、行数等可能随时改变,可以watch这些属性变化,然后重新计算一次截取长度。另一方面,对于页面布局而言,可能会因为其它页面元素的增删或者样式改变,导致页面布局变动,影响到文本容器的宽度,此时也应该重新计算一次截取长度。
监听文本容器宽度的变化,可以考虑使用ResizeObserver来监听,但是这个接口的兼容性不够好(IE各个版本都不支持),因此选择了一个npm库element-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 < content.length) || btnShow">
{{ ellipsisText }}
<span class="ellipsis-btn" @click="clickBtn">{{ btnText }}</span>
</slot>
</div>
</template>
<script> 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)
},
}
} </script>
在代码实现中refresh函数用于计算截取长度,在文本内容、rows属性等发生改变或者文本容器尺寸改变时将被调用。每次refresh调用会异步地递归调用多次checkLoop,refresh可能重新调用,新的refresh调用将结束之前的checkLoop的调用。
现在的实现方案并不支持内容是HTML文本,如果需要支持HTML文本,问题将复杂许多。主要在于HTML字符串的解析和截断,不像文本字字符串那么简单。不过或许可以借助浏览器的Range API 来实现截断位置的定位,Range的insertNode以及setStart接口可以将“...查看全部”插入到指定位置,而如果插入位置刚好符合需要,则可以通过Range.cloneContents()")接口取得截取HTML字符串的相关内容,理论上是可行的,不过具体细节以及处理效率得实践后才知道。
上述实现方案中,每一次截取都需要浏览器重新渲染DOM,即重绘。重绘的影响还比较小,而如果截取的字符串行数发生改变,还会引发文本容器的高度变化,这时候就会导致浏览器回流,而文本容器在文档流中,回流将会影响整个文档。
想解决这个问题,可以使用一个脱离文档流的元素来进行字符串动态截断后的渲染与判断,布局就类似上述的textarea。因为不在文档流中,回流的影响范围就会减少到该元素自身。获得截断长度后再截断文本,渲染到真正的文本容器即可。本文仅作为一个简单的原理概述的示例,没有做这个处理,对具体细节感兴趣的同学,可以查看github仓库代码。
如果实现单行文本的溢出显示省略号同学们应该都知道用text-overflow:ellipsis属性来,当然还需要加宽度width属来兼容部分浏览。但是这个属性只支持单行文本的溢出显示省略号,如果我们要实现多行文本溢出显示省略号呢。
在我们的日常开发工作中,文本溢出截断省略是很常见的一种需考虑的业务场景细节。看上去 “稀松平常” ,但在实现上却有不同的区分,是单行截断还是多行截断?多行的截断判断是基于行数还是基于高度?这些问题之下,都有哪些实现方案?
首先,用以下命令创建一个 React App。然后在 Visual Studio Code 中打开新创建的项目,并使用以下命令安装 Bootstrap:打开 index.js 文件并导入Bootstrap。
CSS为HTML标记语言提供了一种样式描述,定义了其中元素的显示方式。下面我们来看一下css如何设置文本超出几行显示省略号。
通过css可以使对应div标签内的文字换行或不换行设置操作,那么该如何设置不换行呢?下面我们来看一下css中不换行的代码是什么?
文本溢出我们经常用到的应该就是text-overflow:ellipsis了,相信大家也很熟悉,但是对于多行文本的溢出处理确接触的不是很多,最近在公司群里面有同事问到,并且自己也遇到过这个问题,所以专门研究过这个问题。
经常可以看到某些网站网页上的文字无法被选中,出了js控制,通过CSS样式user-select和z-index两个属性都可导致无法复制文字;user-selectuser-select
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!