共计 4860 个字符,预计需要花费 13 分钟才能阅读完成。
本文公布于我的集体网站: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 即截取长度,须要动静计算。
想通过上述计划实现,有几个问题须要解决:
-
- 怎么判断文字是否超过指定行数
- 如何计算字符串截取长度
- 动静响应,包含响应页面布局变动、字符串变动、指定行数变动等
上面具体钻研一下这些问题。
1. 怎么判断一段文字是否超过指定行数?
首先解决一个小问题:如何计算指定行数的高度?我首先想到的是应用 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
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 各个版本都不反对),因而抉择了一个 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 的调用。
四、其它
1. 反对 HTML 串的思考
当初的实现计划并不反对内容是 HTML 文本,如果须要反对 HTML 文本,问题将简单许多。次要在于 HTML 字符串的解析和截断,不像文本字字符串那么简略。不过或者能够借助浏览器的 Range API 来实现截断地位的定位,Range 的 insertNode 以及 setStart 接口能够将“… 查看全副”插入到指定地位,而如果插入地位刚好合乎须要,则能够通过 Range.cloneContents()
“) 接口获得截取 HTML 字符串的相干内容,实践上是可行的,不过具体细节以及解决效率得实际后才晓得。
2. 缩小浏览器回流的影响
上述实现计划中,每一次截取都须要浏览器从新渲染 DOM,即重绘。重绘的影响还比拟小,而如果截取的字符串行数产生扭转,还会引发文本容器的高度变动,这时候就会导致浏览器回流,而文本容器在文档流中,回流将会影响整个文档。
想解决这个问题,能够应用一个脱离文档流的元素来进行字符串动静截断后的渲染与判断,布局就相似上述的 textarea。因为不在文档流中,回流的影响范畴就会缩小到该元素本身。取得截断长度后再截断文本,渲染到真正的文本容器即可。本文仅作为一个简略的原理概述的示例,没有做这个解决,对具体细节感兴趣的同学,能够查看 github 仓库代码。