CSS 實現多行文本 “展開收起”

多行文本展開收起是一個很常見的交互, 如下圖演示

 實現這一類佈局和交互難點主要有以下幾點:

說實話,之前單獨看這個佈局,即使藉助 JavaScript 也不是一件容易的事啊(需要計算文字寬度動態截取文本,vue-clamp[1] 就是這麼做的),更別說下面的交互和判斷邏輯了,不過經過我的一番琢磨,其實純 CSS 也能完美實現的,下面就一步一步來看看如何實現吧~

一、位於右下角的 “展開收起” 按鈕

==========================

很多設計同學都喜歡這樣的設計,把按鈕放在右下角,和文本混合在一起,而不是單獨一行,視覺上可能更加舒適美觀。先看看多行文本截斷吧,這個比較簡單

1. 多行文本截斷

假設有這樣一個 html 結構

<div>
浮動元素是如何定位的
正如我們前面提到的那樣,當一個元素浮動之後,它會被移出正常的文檔流,然後向左或者向右平移,一直平移直到碰到了所處的容器的邊框,或者碰到另外一個浮動的元素。
</div>

多行文本超出省略大家應該很熟悉這個了吧,主要用到用到 line-clamp,關鍵樣式如下

.text {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

2. 右下角環繞效果

提到文本環繞效果,一般能想到 浮動 float ,沒錯千萬不要以爲浮動已經是過去式了,具體的場景還是很有用的;比如下面放一個按鈕,然後設置浮動

<div>
  <button>展開</button>
  浮動元素是如何定位的
正如我們前面提到的那樣,當一個元素浮動之後,它會被移出正常的文檔流,然後向左或者向右平移,一直平移直到碰到了所處的容器的邊框,或者碰到另外一個浮動的元素。
</div>
.btn {
  float: left;
  /*其他裝飾樣式*/
}

如果設置右浮動

.btn {
  float: right;
  /*其他裝飾樣式*/
}

這時已經有了環繞的效果了,只是位於右上角,如何將按鈕移到右下角呢?先嚐試一下 margin

.btn {
  float: right;
  margin-top: 50px;
  /*其他裝飾樣式*/
}

可以看到,雖然按鈕到了右下角,但是文本卻沒有環繞按鈕上方的空間,空出了一大截,無能爲力了嗎?

雖然 margin 不能解決問題,但是整個文本還是受到了浮動按鈕的影響,如果有多個浮動元素會怎麼樣呢?這裏用僞元素來 ::before 代替

.text::before{
  content: '';
  float: right;
  width: 10px;
  height: 50px;/*先隨便設置一個高度*/
  background: red
}

現在按鈕到了僞元素的左側,如何移到下面呢?很簡單,清除一下浮動 clear: both; 就可以了

.btn {
  float: right;
  clear: both;
  /*其他裝飾樣式*/
}

可以看到,現在文本是完全環繞在右側的兩個浮動元素了,只要把紅色背景的僞元素寬度設置爲 0(或者不設置寬度,默認就是 0),就實現了右下角環繞的效果

.text::before{
  content: '';
  float: right;
  width: 0; /*設置爲0,或者不設置寬度*/
  height: 50px;/*先隨便設置一個高度*/
}

3. 動態高度

上面雖然完成了右下加環繞,但是高度是固定的,如何動態設置呢?這裏可以用到 calc 計算,用整個容器高度減去按鈕的高度即可,如下

.text::before{
  content: '';
  float: right;
  width: 0;
  height: calc(100% - 24px);
}

很可惜,好像並沒有什麼效果,打開控制檯看看,結果發現 calc(100% - 24px) 計算高度爲 0

原因其實很容易想到,就是 高度 100% 失效 的問題,關於這類問題網上的分析有很多,通常的解決方式是給父級指定一個高度,但是這裏的高度是動態變化的,而且還有展開狀態,高度更是不可預知,所以設置高度不可取。

除此之外,其實還有另一種方式,那就是利用 flex 佈局。大概的方法就是在 **flex 佈局 **的子項中,可以通過百分比來計算變化高度,具體可參考 w3.org 中關於 css-flexbox[2] 的描述

If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.

因此,這裏需要給 .text 包裹一層,然後設置 display: flex

<div>
  <div>
    <button>展開</button>
    浮動元素是如何定位的
  正如我們前面提到的那樣,當一個元素浮動之後,它會被移出正常的文檔流,然後向左或者向右平移,一直平移直到碰到了所處的容器的邊框,或者碰到另外一個浮動的元素。
  </div>
</div>
.wrap{
  display: flex;
}

實踐下來,display: grid 和 display: -webkit-box 同樣有效,原理類似

這樣下來,剛纔的計算高度就生效了,改變文本的行數,同樣位於右下角~

除此之外,動態高度也可以採用負的 margin 來實現(性能會比 calc 略好一點)

.text::before{
  content: '';
  float: right;
  width: 0;
  /*height: calc(100% - 24px);*/
  height: 100%;
  margin-bottom: -24px;
}

到這裏,右下角環繞的效果就基本完成,省略號也是位於展開按鈕之前的,完整代碼可以查看 codepen 右下角多行展開環繞效果 [3]

4. 其他瀏覽器的兼容處理


上面的實現是最完美的處理方式。原本以爲兼容性沒什麼大問題的,畢竟只用到了文本截斷和浮動,-webkit-line-clamp 雖然是 -webkit- 前綴,不過 firefox 也是支持的,打開一看傻了眼,safarifirefox 居然全亂了!

這就有點難受了,前面那麼多努力都白費了嗎?不可能不管這兩個,不然就只能是 demo 了,無法用於生產環境。

趕緊打開控制檯看看是什麼原因。一番查找,結果發現是 display: -webkit-box,設置該屬性後,原本的文本好像變成了一整塊,浮動元素也無法產生環繞效果,去掉之後浮動就正常了

那麼問題來了:沒有 display: -webkit-box 怎麼實現多行截斷呢 ?

其實上面的努力已經實現了右下角環繞的效果,如果在知道行數的情況下設置一個最大高度,是不是也完成了多行截斷呢?爲了便於設置高度,可以添加一個行高 line-height,如果需要設置成 3 行,那高度就設置成 line-height * 3

.text {
  /*
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  */
  line-height: 1.5;
  max-height: 4.5em;
  overflow: hidden;
}

爲了方便更好的控制行數,這裏可以把常用的行數通過屬性選擇器獨立出來(通常不會太多),如下

[line-clamp="1"] {
  max-height: 1.5em;
}
[line-clamp="2"] {
  max-height: 3em;
}
[line-clamp="3"] {
  max-height: 4.5em;
}
...
<!--3行-->
<div line-clamp="3">
...
</div>
<!--5行-->
<div line-clamp="5">
 ...
</div>

可以看到基本上正常了,除了沒有省略號,現在加上省略號吧,跟在展開按鈕之前就可以了,可以用僞元素實現

.btn::before{
  content: '...';
  position: absolute;
  left: -10px;
  color: #333;
  transform: translateX(-100%)
}

這樣,SafariFirefox 的兼容佈局基本上就完成了,完整代碼可以查看 codepen 右下角多行展開環繞效果(全兼容)[4]

二、“展開” 和 “收起”  兩種狀態

===========================

提到 CSS 狀態切換,大家都能想到 input type="checkbox" 吧。這裏我們也需要用到這個特性,首先加一個 input ,然後把之前的 button 換成 label ,並且通過 for 屬性關聯起來

<div>
  <input type="checkbox">
  <div>
    <label for="exp">展開</label>
    浮動元素是如何定位的
  正如我們前面提到的那樣,當一個元素浮動之後,它會被移出正常的文檔流,然後向左或者向右平移,一直平移直到碰到了所處的容器的邊框,或者碰到另外一個浮動的元素。
  </div>
</div>

這樣,在點擊 label 的時候,實際上是點擊了 input 元素,現在來添加兩種狀態,分別是隻顯示 3 行和不做行數限制

.exp:checked+.text{
  -webkit-line-clamp: 999; /*設置一個足夠大的行數就可以了*/
}

兼容版本可以直接設置最大高度 max-height 爲一個較大的值,或者直接設置爲 none

.exp:checked+.text{
  max-height: none;
}

這裏還有一個小問題,“展開” 按鈕在點擊後應該變成 “收起”,如何修改呢?

有一個技巧,凡是碰到需要動態修改內容的,都可以使用僞類 content 生成技術,具體做法就是去除或者隱藏按鈕裏面的文字,採用僞元素生成

<label class="btn" for="exp"></label><!--去除按鈕文字--
.btn::after{
  content:'展開' /*採用content生成*/
}

添加 :checked 狀態

.exp:checked+.text .btn::after{
  content:'收起'
}

兼容版本由於前面的省略號是模擬出來的,不能自動隱藏,所以需要額外來處理

.exp:checked+.text .btn::before {
    visibility: hidden; /*在展開狀態下隱藏省略號*/
}

基本和本文開頭的效果一致了,完整代碼可以查看 codepen 多行展開收起交互 [5],兼容版本可以查看 codepen 多行展開收起交互(全兼容)[6]

還有一點,如果給 max-height 設置一個合適的值,注意,是合適的值,具體原理可以參考 CSS 奇技淫巧:動態高度過渡動畫 [7],還能加上過渡動畫

.text{
  transition: .3s max-height;
}
.exp:checked+.text{
  max-height: 200px; /*超出最大行高度就可以了*/
}

三、文本行數的判斷

=================

上面的交互已經基本滿足要求了,但是還是會有問題。比如當文本較少時,此時是沒有發生截斷,也就是沒有省略號的,但是 “展開” 按鈕卻仍然位於右下角,如何隱藏呢?

通常 js 的解決方式很容易,比較一下元素的 scrollHeightclientHeight 即可,然後添加相對應的類名。下面是僞代碼

if (el.scrollHeight > el.clientHeight) {
  // 文本超出了
  el.classList.add('trunk')
}

那麼,CSS 如何實現這類判斷呢?

可以肯定的是,CSS 是沒有這類邏輯判斷,大多數我們都需要從別的角度,採用 “障眼法” 來實現。比如在這個場景,當沒有發生截斷的時候,表示文本完全可見了,這時,如果在文本末尾添加一個元素(紅色小方塊),爲了不影響原有佈局,這裏設置了絕對定位

.text::after {
    content: '';
    width: 10px;
    height: 10px;
    position: absolute;
    background: red;
}

可以看到,這裏的紅色小方塊是完全跟隨省略號的。當省略號出現時,紅色小方塊必定消失,因爲已經被擠下去了,這裏把父級 overflow: hidden 暫時隱藏就能看到是什麼原理了

然後,可以把剛纔這個紅色的小方塊設置一個足夠大的尺寸,比如 100% * 100%

.text::after {
    content: '';
    width: 100%;
    height: 100%;
    position: absolute;
    background: red;
}

可以看到,紅色的塊塊把右下角的都覆蓋了,現在把背景改爲白色(和父級同底色),父級  overflow: hidden 重新加上

.text::after {
    content: '';
    width: 100%;
    height: 100%;
    position: absolute;
    background: #fff;
}

現在看看點擊展開的效果吧

現在展開以後,發現按鈕不見(被剛纔那個僞元素所覆蓋,並且也點擊不了),如果希望點擊以後仍然可見呢?添加一下 :checked 狀態即可,在展開時隱藏覆蓋層

.exp:checked+.text::after{
    visibility: hidden;
}

這樣,就實現了在文字較少的情況下隱藏展開按鈕的功能

最終完整代碼可以查看 codepen 多行展開收起自動隱藏 [8],兼容版本可以查看 codepen 多行展開收起自動隱藏(全兼容)[9]

需要注意的是,兼容版本可以支持到 IE 10+(這就過分了啊,居然還支持 IE),但是由於 IE 不支持 codepen,所以測試 IE 可以自行復制在本地測試。

==================================================================================================================================================

四、總結和說明

===============

總的來說,重點還是在佈局方面,交互其實相對容易,整體實現的成本其實是很低的,也沒有比較生僻的屬性,除了佈局方面 -webkit-box 貌似有點 bug (畢竟是 -webkit- 內核,火狐只是借鑑了過來,難免有些問題),幸運的是可以通過另一種方式實現多行文本截斷效果,兼容性相當不錯,基本上全兼容(IE10+),這裏整理一下實現重點:

多行文本展開收起效果可以說是業界一個老大難的問題了,有很多 js 解決方案,但是感覺都不是很完美,希望這個全新思路的 CSS 解決方式能給各位帶來不一樣的啓發,感謝閱讀,歡迎點贊、收藏、轉發~

References

[1] vue-clamp: https://justineo.github.io/vue-clamp/demo/?lang=zh&fileGuid=XtpJhGpvWxj6qcTr_[2]__ css-flexbox: https://www.w3.org/TR/css-flexbox-1/#algo-stretch?fileGuid=XtpJhGpvWxj6qcTr
__[3]__ codepen 右下角多行展開環繞效果: https://codepen.io/xboxyan/pen/ExWaBJO?fileGuid=XtpJhGpvWxj6qcTr
__[4]__ codepen 右下角多行展開環繞效果(全兼容): https://codepen.io/xboxyan/pen/dyvYNxr?fileGuid=XtpJhGpvWxj6qcTr
__[5]__ codepen 多行展開收起交互: https://codepen.io/xboxyan/pen/XWMbJeQ?fileGuid=XtpJhGpvWxj6qcTr
__[6]__ codepen 多行展開收起交互(全兼容): https://codepen.io/xboxyan/pen/OJpypmR?fileGuid=XtpJhGpvWxj6qcTr
__[7]__ CSS 奇技淫巧:動態高度過渡動畫: https://github.com/chokcoco/iCSS/issues/91?fileGuid=XtpJhGpvWxj6qcTr
__[8]__ codepen 多行展開收起自動隱藏: https://codepen.io/xboxyan/pen/eYvNvYK?fileGuid=XtpJhGpvWxj6qcTr
__[9]__ codepen 多行展開收起自動隱藏(全兼容): https://codepen.io/xboxyan/pen/LYWpWzK?fileGuid=XtpJhGpvWxj6qcTr_

iCSS,不止於 CSS,如果你也對各種新奇有趣的前端(CSS)知識感興趣,歡迎關注 。同時如果你有任何想法疑問,歡迎加我的微信 「****coco1s 」,一起探討!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/geFAIIO3IPAS3rVahYV0yw