文本內容超過N行折疊並顯示“...查看全部”


一、需求描述

長度不定的一段文字,最多顯示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的調用。

https://www.houdianzi.com/ logo設計公司

四、其它

1. 支持html串的考慮

現在的實現方案並不支持內容是html文本,如果需要支持HTML文本,問題將復雜許多。主要在於HTML字符串的解析和截斷,不像文本字字符串那么簡單。不過或許可以借助瀏覽器的Range API 來實現截斷位置的定位,Range的insertNode以及setStart接口可以將“...查看全部”插入到指定位置,而如果插入位置剛好符合需要,則可以通過Range.cloneContents()")接口取得截取HTML字符串的相關內容,理論上是可行的,不過具體細節以及處理效率得實踐后才知道。

2. 減少瀏覽器回流的影響

上述實現方案中,每一次截取都需要瀏覽器重新渲染DOM,即重繪。重繪的影響還比較小,而如果截取的字符串行數發生改變,還會引發文本容器的高度變化,這時候就會導致瀏覽器回流,而文本容器在文檔流中,回流將會影響整個文檔。

想解決這個問題,可以使用一個脫離文檔流的元素來進行字符串動態截斷后的渲染與判斷,布局就類似上述的textarea。因為不在文檔流中,回流的影響范圍就會減少到該元素自身。獲得截斷長度后再截斷文本,渲染到真正的文本容器即可。本文僅作為一個簡單的原理概述的示例,沒有做這個處理,對具體細節感興趣的同學,可以查看github倉庫代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM