在代码实现中refresh函数用于计算截取长度,在文本内容、rows属性等发生改变或者文本容器尺寸改变时将被调用。每次refresh调用会异步地递归调用多次checkLoop,refresh可能重新调用,新的refresh调用将结束之前的checkLoop的调用。
<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>
支持HTML串的考虑
现在的实现方案并不支持内容是HTML文本,如果需要支持HTML文本,问题将复杂许多。主要在于HTML字符串的解析和截断,不像文本字字符串那么简单。不过或许可以借助浏览器的Range API [4]来实现截断位置的定位,Range的insertNode以及setStart接口可以将“...查看全部”插入到指定位置,而如果插入位置刚好符合需要,则可以通过[Range.cloneContents()](https://developer.mozilla.org/zh-CN/docs/Web/API/Range/cloneContents "Range.cloneContents( "Range.cloneContents()")")接口取得截取HTML字符串的相关内容,理论上是可行的,不过具体细节以及处理效率得实践后才知道。减少浏览器回流的影响
上述实现方案中,每一次截取都需要浏览器重新渲染DOM,即重绘。重绘的影响还比较小,而如果截取的字符串行数发生改变,还会引发文本容器的高度变化,这时候就会导致浏览器回流,而文本容器在文档流中,回流将会影响整个文档。
想解决这个问题,可以使用一个脱离文档流的元素来进行字符串动态截断后的渲染与判断,布局就类似上述的textarea。因为不在文档流中,回流的影响范围就会减少到该元素自身。获得截断长度后再截断文本,渲染到真正的文本容器即可。本文仅作为一个简单的原理概述的示例,没有做这个处理,对具体细节感兴趣的同学,可以查看github仓库代码。