<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)eb技術(shù)】810- 超過(guò)N行如何折疊并顯示“...查看全部”?

          共 5315字,需瀏覽 11分鐘

           ·

          2020-12-18 03:10

          ?

          多行文本超過(guò)指定行數(shù)隱藏超出部分并顯示“...查看全部”是一個(gè)常遇到的需求,網(wǎng)上也有人實(shí)現(xiàn)過(guò)類(lèi)似的功能,不過(guò)還是想自己寫(xiě)寫(xiě)看,

          ?

          于是就寫(xiě)了一個(gè)Vue的組件,本文簡(jiǎn)單介紹一下實(shí)現(xiàn)思路。

          遇到這個(gè)需求的同學(xué)可以嘗試一下這個(gè)組件,支持npm安裝使用:

          一、需求描述

          長(zhǎng)度不定的一段文字,最多顯示n行(比如3行),不超過(guò)n行正常顯示;超過(guò)n行則在最后一行尾部顯示“展開(kāi)”或“查看全部”之類(lèi)的按鈕,點(diǎn)擊按鈕則展開(kāi)顯示全部?jī)?nèi)容,或者跳轉(zhuǎn)到其它頁(yè)面展示所有內(nèi)容。

          預(yù)期效果如下:

          二、實(shí)現(xiàn)原理

          ?

          純CSS很難完美實(shí)現(xiàn)這個(gè)功能,所以還得借助JS來(lái)實(shí)現(xiàn),實(shí)現(xiàn)思路大體相似,都是判斷內(nèi)容是否超過(guò)指定行數(shù),超過(guò)則截取字符串的前x個(gè)字符,然后然后和“...查看全部”拼接在一起,這里的x即截取長(zhǎng)度,需要?jiǎng)討B(tài)計(jì)算。

          ?

          想通過(guò)上述方案實(shí)現(xiàn),有幾個(gè)問(wèn)題需要解決:

          • 怎樣判斷文字是否超過(guò)指定行數(shù)
          • 如何計(jì)算字符串截取長(zhǎng)度
          • 動(dòng)態(tài)響應(yīng),包括響應(yīng)頁(yè)面布局變動(dòng)、字符串變化、指定行數(shù)變化等
          • 下面具體研究一下這些問(wèn)題。

          1. 怎樣判斷一段文字是否超過(guò)指定行數(shù)?

          首先解決一個(gè)小問(wèn)題:如何計(jì)算指定行數(shù)的高度?我首先想到的是使用textarea的rows屬性,指定行數(shù),然后計(jì)算textarea撐起的高度。另一個(gè)方法是將行高的計(jì)算值與行數(shù)相乘,即得到指定行數(shù)的高度,這個(gè)辦法我沒(méi)嘗試過(guò),但是想必可行。

          解決了指定行數(shù)高度的問(wèn)題,計(jì)算一段文字是否超過(guò)指定行數(shù)就很容易了。

          我們可以將指定行數(shù)的textarea使用絕對(duì)定位absolute脫離文檔流,放到文字的下方,然后通過(guò)文本容器的底部與textarea的底部相比較,如果文本容器的底部更靠下,說(shuō)明超過(guò)指定行數(shù)。

          這個(gè)判斷可以通過(guò)getBoundingClientRect接口獲取到兩個(gè)容器的位置、大小信息,然后比較位置信息中的bottom屬性即可。

          可以這樣設(shè)計(jì)DOM結(jié)構(gòu):

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

          然后使用CSS控制textarea,使其脫離文檔流并且不能被看到以及被觸發(fā)鼠標(biāo)事件等(textarea標(biāo)簽中的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.如何計(jì)算字符串截取長(zhǎng)度x——雙邊逼近法(二分思想)

          ?

          只要可以判斷一段文字是否超過(guò)指定行數(shù),那我們就可以動(dòng)態(tài)地嘗試截取字符串,直到找到合適的截?cái)嚅L(zhǎng)度x。

          ?

          這個(gè)長(zhǎng)度滿(mǎn)足從x的位置截?cái)嘧址鞍氩糠?“...查看全部”等文字剛好不會(huì)超出指定行數(shù)N,但是多截取一個(gè)字,則會(huì)超出N行。

          最直觀的想法就是直接遍歷,讓x從0開(kāi)始增長(zhǎng)到顯示文本總長(zhǎng)度,對(duì)于每個(gè)x值,都計(jì)算一次文字是否超過(guò)N行,沒(méi)超過(guò)則加繼續(xù)遍歷,超過(guò)則獲得了合適的長(zhǎng)度x - 1,跳出循環(huán)。當(dāng)然也可以讓x從文本總長(zhǎng)度遞減遍歷。

          不過(guò)這里最大的問(wèn)題在于瀏覽器的回流和重繪。因?yàn)槲覀兠看谓厝∽址夹枰獮g覽器重新渲染出來(lái)才能得到是否超過(guò)N行,這過(guò)程中就觸發(fā)了瀏覽器的重繪或回流,每次循環(huán)都會(huì)觸發(fā)一次。

          而對(duì)于正常的需求來(lái)說(shuō),假設(shè)N取值是3,那很可能每次計(jì)算會(huì)導(dǎo)致50次以上的重繪或回流,這中間消耗的性能還是非常大的,不小心可能就是幾十毫秒甚至上百毫秒。這個(gè)計(jì)算過(guò)程應(yīng)該在一個(gè)任務(wù)(即常說(shuō)的”宏任務(wù)“)中完成,否則計(jì)算過(guò)程中會(huì)出現(xiàn)顯示閃動(dòng)的”異常“情況,所以可以說(shuō)計(jì)算過(guò)程是阻塞的,因此計(jì)算的總時(shí)間一定要控制到非常低,即要減少計(jì)算的次數(shù)。

          可以考慮使用"雙邊逼近法"(或稱(chēng)”二分法“)查找合適的截取長(zhǎng)度x,大大減少?lài)L試的次數(shù)。

          第一次先以文本長(zhǎng)度為截取長(zhǎng)度,計(jì)算是否超過(guò)N行,沒(méi)超過(guò)則停止計(jì)算;超過(guò)則取1/2長(zhǎng)度進(jìn)行截取,如果此時(shí)沒(méi)超過(guò)N行,則在1/2長(zhǎng)度到文本長(zhǎng)度之間繼續(xù)二分查找,如果超過(guò)則在0到1/2文本長(zhǎng)度中繼續(xù)二分查找。

          直到查找區(qū)間開(kāi)始值與結(jié)束值相差為1,則開(kāi)始值即為所求。具體實(shí)現(xiàn)可以看下文中的完整代碼。

          3.監(jiān)聽(tīng)頁(yè)面變動(dòng)

          對(duì)于Vue項(xiàng)目來(lái)說(shuō),傳入組件的字符串、行數(shù)等可能隨時(shí)改變,可以watch這些屬性變化,然后重新計(jì)算一次截取長(zhǎng)度。

          另一方面,對(duì)于頁(yè)面布局而言,可能會(huì)因?yàn)槠渌?yè)面元素的增刪或者樣式改變,導(dǎo)致頁(yè)面布局變動(dòng),影響到文本容器的寬度,此時(shí)也應(yīng)該重新計(jì)算一次截取長(zhǎng)度。

          監(jiān)聽(tīng)文本容器寬度的變化,可以考慮使用ResizeObserver來(lái)監(jiān)聽(tīng),但是這個(gè)接口的兼容性不夠好(IE各個(gè)版本都不支持),因此選擇了一個(gè)npm庫(kù)element-resize-detector來(lái)監(jiān)測(cè)(非常好用?)。

          三、代碼實(shí)現(xiàn)

          <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:?'展開(kāi)'
          ????},
          ????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?()?{?//?用一個(gè)計(jì)算屬性來(lái)統(tǒng)一觀察需要關(guān)注的屬性變化
          ??????return?[this.content,?this.btnText,?this.ellipsisText,?this.rows,?this.btnShow]
          ????}
          ??},
          ??watch:?{
          ????watchData:?{
          ??????immediate:?true,
          ??????handler?()?{
          ????????this.refresh()
          ??????}
          ????},
          ??},
          ??mounted?()?{
          ????//?監(jiān)聽(tīng)尺寸變化
          ????observer.listenTo(this.$refs.shadow,?()?=>?this.refresh())
          ??},
          ??beforeDestroy?()?{
          ????observer.uninstall(this.$refs.shadow)
          ??},
          ??methods:?{
          ????refresh?()?{?//?計(jì)算截取長(zhǎng)度,存儲(chǔ)于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))
          ????},
          ????//?展開(kāi)按鈕點(diǎn)擊事件向外部emit
          ????clickBtn?(event)?{
          ??????this.$emit('click-btn',?event)
          ????},
          ??}
          }

          在代碼實(shí)現(xiàn)中refresh函數(shù)用于計(jì)算截取長(zhǎng)度,在文本內(nèi)容、rows屬性等發(fā)生改變或者文本容器尺寸改變時(shí)將被調(diào)用。

          每次refresh調(diào)用會(huì)異步地遞歸調(diào)用多次checkLoop,refresh可能重新調(diào)用,新的refresh調(diào)用將結(jié)束之前的checkLoop的調(diào)用。

          四、其它

          1. 支持HTML串的考慮

          現(xiàn)在的實(shí)現(xiàn)方案并不支持內(nèi)容是HTML文本,如果需要支持HTML文本,問(wèn)題將復(fù)雜許多。主要在于HTML字符串的解析和截?cái)?,不像文本字字符串那么?jiǎn)單。

          不過(guò)或許可以借助瀏覽器的Range API 來(lái)實(shí)現(xiàn)截?cái)辔恢玫亩ㄎ唬?code style>Range的insertNode以及setStart接口可以將“...查看全部”插入到指定位置,而如果插入位置剛好符合需要,則可以通過(guò)Range.cloneContents()接口取得截取HTML字符串的相關(guān)內(nèi)容,理論上是可行的,不過(guò)具體細(xì)節(jié)以及處理效率得實(shí)踐后才知道。

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

          上述實(shí)現(xiàn)方案中,每一次截取都需要瀏覽器重新渲染DOM,即重繪。

          重繪的影響還比較小,而如果截取的字符串行數(shù)發(fā)生改變,還會(huì)引發(fā)文本容器的高度變化,這時(shí)候就會(huì)導(dǎo)致瀏覽器回流,而文本容器在文檔流中,回流將會(huì)影響整個(gè)文檔。

          想解決這個(gè)問(wèn)題,可以使用一個(gè)脫離文檔流的元素來(lái)進(jìn)行字符串動(dòng)態(tài)截?cái)嗪蟮匿秩九c判斷,布局就類(lèi)似上述的textarea。

          因?yàn)椴辉谖臋n流中,回流的影響范圍就會(huì)減少到該元素自身。獲得截?cái)嚅L(zhǎng)度后再截?cái)辔谋荆秩镜秸嬲奈谋救萜骷纯伞?/p>

          本文僅作為一個(gè)簡(jiǎn)單的原理概述的示例,沒(méi)有做這個(gè)處理,對(duì)具體細(xì)節(jié)感興趣的同學(xué),可以查看github倉(cāng)庫(kù)代碼。

          組件地址:

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

          在線體驗(yàn):

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

          文章:

          https://wintc.top/article/58

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(mén)(上)||?Webpack4 入門(mén)(下)
          6.?MobX 入門(mén)(上)?||??MobX 入門(mén)(下)
          7. 80+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章

          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色在线视频网站 | 国产精品麻豆 | 九色中文在线 | 波多野结衣AV二区 | 日本一级片在线播放 |