高度自適應(yīng)輸入框的清晰解題思路

前言

業(yè)務(wù)開發(fā)中,經(jīng)常會(huì)遇到文本輸入框高度隨著輸入內(nèi)容高度變化的情況,下面我們來詳細(xì)說明一下實(shí)現(xiàn)這種輸入框的方案和解題思路

方案一為一種擴(kuò)展思路,僅供參考
方案二為常規(guī)思路,急著用的小伙伴建議直接看二
第三個(gè)板塊 附上了react native和react native to web的代碼實(shí)現(xiàn)方案

※方案一:contenteditable屬性法

  • contenteditable屬性表示元素是否可編輯,變?yōu)榭删庉嫚顟B(tài)的元素還保留其原有的特性,屬性值為如下兩者之一
    -true或空字符串,表示元素是可編輯的;
    -false表示元素不是可編輯的;
  • 該屬性是一個(gè)枚舉屬性,而非布爾屬性。這意味著必須顯式設(shè)置其值為 truefalse 或空字符串中的一個(gè),最好不要簡(jiǎn)寫為<label contenteditable>Example Label</label>
  • 正確的用法是 :
<element contenteditable="value">   --value=true/false
  • ??分析
<style type="text/css">
    .container {
        padding: 20px;
    }
    .auto-input {
        min-height: 100px;
        font-size: 30px;
        border: 1px solid red;
    }
</style>

<body>
    <h1>contenteditable實(shí)現(xiàn)的高度自適應(yīng)輸入框</h1>
    <div class="container">
      <div class="auto-input" contenteditable="true" id="auto-input"></div>
    </div>
</body>
  • 注意事項(xiàng):如想要設(shè)置文本輸入框的默認(rèn)高度,設(shè)置min-height即可,文本輸入框同時(shí)支持focus,blur事件,但是即使外表偽裝的和textarea一模一樣,還是有需要注意的坑點(diǎn),試著分別輸入和復(fù)制內(nèi)容,查看dom節(jié)點(diǎn)的變化:
    以下為手打內(nèi)容和復(fù)制黏貼的區(qū)別
image

可以看出內(nèi)容其實(shí)并不是真正的純文本,而是帶有樣式的富文本格式,黏貼進(jìn)去的內(nèi)容呈現(xiàn)的還是復(fù)制前的樣式,我們可以通過innerText獲取到里面的純文本內(nèi)容

image

保留此功能很適合展示圖片,復(fù)制一張圖片進(jìn)入輸入框中可直接展示

image
  • 純文本設(shè)置方法:為了完成和textarea同樣的作用,我們可以在輸入時(shí)進(jìn)行過濾,保證輸入的是純文本文件,有兩種辦法
  1. 在css中設(shè)置
div[contenteditable] {
    user-modify: read-write-plaintext-only
}
user-modify  可以控制普通元素是否可讀寫
user-modify: read-only; // 只讀    
user-modify: read-write; // 可讀寫,支持富文本   
user-modify: write-only; // 只寫,支持的瀏覽器很少     
user-modify: read-write-plaintext-only;//可讀寫,純文本,目前只有webkit內(nèi)核瀏覽器支持      
  1. 在html中給div增加屬性
<div contenteditable="plaintext-only">
  • 結(jié)果分析:
    這種方案的缺點(diǎn)在于,一個(gè)元素加上contenteditable,即使解決了可編輯的問題,但是表單控件的一些特性placeholder,maxlengthautofocus,只能js去輔助完成,在移動(dòng)端會(huì)有一些兼容性的問題

方案二:純textarea文本框?qū)崿F(xiàn)

思路初始化—?jiǎng)?chuàng)建textarea元素

  • 我們預(yù)期通過一個(gè)文本輸入框textarea即可完成高度自適應(yīng),但是實(shí)際表明,如果只是通過textarea和一些簡(jiǎn)單的css方法,設(shè)置textarea的min-height后會(huì)發(fā)現(xiàn),當(dāng)輸入的元素內(nèi)容的高度超過設(shè)置的最小高度時(shí),會(huì)產(chǎn)生滾動(dòng)條,顯然不符合我們的預(yù)期

進(jìn)展1—獲取元素的scrollTopoffsetHeight并設(shè)置高度

  • 既然產(chǎn)生了滾動(dòng)條,那就可以嘗試著獲取文本框的scrollTop,加上文本框的原有高度,在監(jiān)聽到文本框內(nèi)容改變的時(shí)候重新設(shè)置文本框的高度
  • 我們用offsetHeight來獲取文本框的高度,該屬性包含文本框的border + padding + content的高度,因此我們要在css中將textarea設(shè)置為border-box,方便設(shè)置的時(shí)候統(tǒng)一
  • 最后元素高度的設(shè)置后只是相當(dāng)于增加了scrollTop部分的值
  • 實(shí)現(xiàn)步驟如下:
    1. input監(jiān)聽文本框內(nèi)容的改變
    2. 獲取文本框的滾動(dòng)距離scrollTop
    3. 獲取文本框的高度offsetHeight
    4. 設(shè)置文本框的新heightscrollTop+offsetHeight
  • ??分析:
css:
    <style>
      body,
      html {
        padding-left: 0.1rem;
        margin: 0;
      }
      .auto-input {
        display: block;
        box-sizing: border-box;
        outline: none;
        resize: none;
        padding: 0;
        width: 2rem;
        height: 30px;
        border: 1px solid #000;
        font-size: 0.2rem;
      }
      .title {
        font-size: 0.2rem;
      }
    </style>
html:
 <body>
    <h1 class="title">textarea js高度自適應(yīng)輸入框</h1>
    <textarea class="auto-input" id="autoInput"></textarea>
  </body>
script:
  <script>
      var autoInput = document.getElementById("autoInput");
      autoInput.addEventListener("input", function() {
        var inputScrollTop = autoInput.scrollTop;
        var inputHeight = autoInput.offsetHeight;
        console.log("inputScrollTop:" + inputScrollTop + 'px')
        autoInput.style.height = inputScrollTop + inputHeight + "px";
      });
    </script>
  • 測(cè)試結(jié)果如下:
image
  • 結(jié)果分析: 在輸入到下一行的時(shí)候,第一個(gè)導(dǎo)致?lián)Q行的字符在觸發(fā)input事件的時(shí)候獲取到的scrollTop的值并未改變,仍然為舊值,直到輸入新一行的第二個(gè)字符的時(shí)候才有所響應(yīng),猜測(cè)原因是scrollTop的獲取時(shí)機(jī)太早導(dǎo)致的問題

進(jìn)展2—增加定時(shí)器延緩執(zhí)行

  • 我們嘗試在js中獲取scrollTop時(shí)外加入定時(shí)器,延緩獲取時(shí)機(jī)
 var autoInput = document.getElementById("autoInput");
      autoInput.addEventListener("input", function() {
          setTimeout(()=>{
            var inputScrollTop = autoInput.scrollTop;
            var inputHeight = autoInput.offsetHeight;
            console.log("inputScrollTop:"+inputScrollTop+'px');
            autoInput.style.height = inputScrollTop + inputHeight + "px";
          },0)
      });
  • 測(cè)試結(jié)果如下:
image
  • 結(jié)果分析:確實(shí)如預(yù)期,添加定時(shí)器延緩了獲取scrollTop的時(shí)機(jī)后,在換行時(shí)獲取到的scrollTop為準(zhǔn)確的。

進(jìn)展3—獲取元素的scrollHeight并設(shè)置高度

  • 雖然在開發(fā)過程中定時(shí)器確實(shí)能解決很多頭疼的問題,但是個(gè)人覺得不是很優(yōu)雅,這里我們嘗試著去獲取另一個(gè)屬性,scrollHeight,對(duì)于滾動(dòng)元素來說scrollHeight代表的是元素原有的高度加上內(nèi)容滾動(dòng)到底部時(shí)的scrollTop,換句話說也就是元素的完整內(nèi)容高度,這個(gè)屬性包含元素的padding,不包含bordermargin
  • 需要注意獲取的scrollHeight本身是不包含邊框的高度的,但是我們要重置的height,因?yàn)樵O(shè)置為border-box,是包含邊框的,因此需要將scollHeight加上邊框后再設(shè)置給textarea的height
  • 實(shí)現(xiàn)步驟如下:
    1. 監(jiān)聽元素變化時(shí)我們?nèi)カ@取scrollHeight
    2. 設(shè)置scrollHeight+border為元素高度
  • ??分析:
script:
    autoInput.addEventListener("input", function() {
        var inputScrollHeight = autoInput.scrollHeight + 2;
        console.log("inputScrollHeight" + inputScrollHeight+"px");
        autoInput.style.height = autoInput.scrollHeight + "px";
      });
  • 測(cè)試結(jié)果:
image
  • 結(jié)果分析:當(dāng)輸入元素內(nèi)容的時(shí)候,確實(shí)可以如我們預(yù)期的,隨著內(nèi)容的增加高度增加,但是刪除的時(shí)候表現(xiàn)卻發(fā)現(xiàn)textarea的高度沒有變化,scrollHeight是用來獲取元素滾動(dòng)的scrollTop,padding,以及內(nèi)容的高度的,那么當(dāng)刪除文本內(nèi)容時(shí)它是否會(huì)發(fā)生變化呢?從上面視頻打印的結(jié)果來看刪除的時(shí)候元素的高度并未發(fā)生變化
    原因如下:如果我們沒有給文本框設(shè)置高度,隨著內(nèi)容的增加
scrollHeight = scrollMaxTop+clientHeight;//元素高度加滾動(dòng)最大距離

其中scrollTop會(huì)隨著內(nèi)容增加可滾動(dòng)的距離變大而增加,所以在添加文字的情況下我們可以發(fā)現(xiàn)scrollHeight會(huì)不斷增大
我們將盒子本身的高度設(shè)置成scrollHeight,

newClientHeight = scrollHeight

刪除的情況下,盒子高度足夠是沒有滾動(dòng)距離的,因此scrollMaxTop為0,newClientHeight不會(huì)再更新,因此盒子也就維持了之前的高度

scrollHeight = 0 + newClientHeight

進(jìn)展4—重置元素的高度

  • 上面因?yàn)閯h除時(shí)scrollHeight并不會(huì)變化導(dǎo)致元素的高度維持在了之前的最大值,那么我們?nèi)绻趧h除元素時(shí),將元素的高度設(shè)置成根據(jù)內(nèi)容自適應(yīng)(auto)/(""),這樣textarea的高度會(huì)被重置成最小化
  • 最小化之后重新獲取到的scrollHeight,又是可以讓當(dāng)前內(nèi)容自適應(yīng)的高度
  • 需要注意auto和""兩者的區(qū)別 如果設(shè)置為auto的話 textarea的高度會(huì)被重置為默認(rèn)高度,默認(rèn)高度不是指css中設(shè)置的高度,而是瀏覽器默認(rèn)的,但是如果設(shè)置為(“”)那么相當(dāng)于清楚的是內(nèi)聯(lián)的高度樣式,并不會(huì)覆蓋css的高度,textarea本身的css高度還是存在的,因此表現(xiàn)不同點(diǎn)在于textarea最小時(shí)的高度,所以這里建議使用"",可以保留原本設(shè)置的高度,但是如果原本設(shè)置的是textarea的min-height而不是height,那兩個(gè)屬性均可
  • 實(shí)現(xiàn)步驟如下:
    1. 監(jiān)聽元素變化時(shí)我們將元素的height設(shè)置為““,目的是為了清楚上一次的高度
    2. 重新獲取元素的scrollHeight
    3. 設(shè)置元素的高度為scrollHeight+border
  • ??分析
autoInput.addEventListener("input", function() {
        autoInput.style.height = "";
        //注意順序  需要先重置 再獲取 
        var inputScrollHeight = autoInput.scrollHeight + 2;
        console.log("inputScrollHeight" + inputScrollHeight+"px");
        autoInput.style.height = inputScrollHeight + "px";
      });
  • 測(cè)試結(jié)果:
image
  • 結(jié)果分析:到此我們終于完成了輸入框的基本功能。但是現(xiàn)在每次Input監(jiān)聽時(shí),我們都會(huì)將元素的高度重置為空,并且每次都會(huì)獲取scrollHeight的高度,無疑會(huì)對(duì)性能有一些損耗,因此我們后面會(huì)嘗試一下優(yōu)化方案。

進(jìn)展5—優(yōu)化方案

  • 我們嘗試增加一些判斷條件來減少不必要的執(zhí)行
  • 在元素內(nèi)容增多的時(shí)候,我們期望只有當(dāng)元素內(nèi)容換行的時(shí)候才進(jìn)行重置操作,但是怎么去監(jiān)聽元素的換行呢,我們可以通過獲取到的scrollHeight,當(dāng)該值增加的時(shí)候再去進(jìn)行設(shè)置高度的操作
  • 另一方面 當(dāng)元素內(nèi)容減少的時(shí)候我們才需要將元素的高度置空,是否也可以通過判斷scrollHeight的值是否變小才進(jìn)行這種判斷呢,答案無疑是否定的,因?yàn)槿绻蝗ピO(shè)置高度為空的話,scrolllHeight的值并不會(huì)發(fā)生變化,目前想到的判斷字符減少的方案為監(jiān)聽輸入的字符個(gè)數(shù)
  • 如果字符數(shù)減少的話我們需要將元素的height置為空,然后重新獲取元素的scrollHeight,
    • 如果減少的字符導(dǎo)致了換行,那么scrollHeight的值會(huì)發(fā)生變化
    • 如果減少的字符沒有導(dǎo)致?lián)Q行,那么scrollHeight沒有發(fā)生變化
  • 無論哪種情況我們都需要去把scrollHeight的值賦值給textarea的height,否則會(huì)變?yōu)閏ss中設(shè)置的最小高度
  • 因此textarea高度重新設(shè)置的的條件為 scrollHeight增加導(dǎo)致?lián)Q行或者文字內(nèi)容減少
  • 實(shí)現(xiàn)步驟如下:
    1. 我們先獲取textarea原本的scrollHeight和字符串長(zhǎng)度
    2. 在監(jiān)聽到內(nèi)容改變的時(shí)候,我們獲取一下新內(nèi)容的長(zhǎng)度
    3. 如果長(zhǎng)度變小 重置文本框的高度
    4. 然后獲取文本框的scrollHeight
    5. 如果scrollHeight變大或者元素的height不存在進(jìn)入到if判斷中
    6. 將之前存儲(chǔ)的文本框的高度設(shè)置成新的方便下一次比較
    7. 設(shè)置文本框的高度為新獲取到的scrollHeight+邊框 結(jié)束if語句
    8. 最后重置下文本框的新內(nèi)容的長(zhǎng)度
  • ??分析
  var lastScrollHeight = autoInput.scrollHeight;
    var lastTextLength = autoInput.value.length;
    autoInput.addEventListener("input", function() {
        var inputTextLength = autoInput.value.length;
        if (inputTextLength < lastTextLength){
            autoInput.style.height = "";
        }
        //注意這句話一定要寫在設(shè)置height為空的后面,否則獲取不到最新的scrollHeight
        var inputScrollHeight = autoInput.scrollHeight;
        //注意如果height為空的話也需要重置高度 否則高度有問題
        if(lastScrollHeight < inputScrollHeight || !autoInput.style.height){
            lastScrollHeight = inputScrollHeight;
            autoInput.style.height = inputScrollHeight + 2 + "px";
        }
        lastTextLength = autoInput.value.length;
    });

  • 測(cè)試結(jié)果:同上
  • 結(jié)果分析:至此已經(jīng)完成了比較完善的自適應(yīng)輸入框

react native和rn2web的實(shí)現(xiàn)方法

RN的實(shí)現(xiàn)方案

  • rn方法提供onContentSizeChange的函數(shù),onContentSizeChange是在內(nèi)容布局改變(如換行)的時(shí)候能獲取到當(dāng)前contentSize中的高度,然后通過state調(diào)整為input的高度
  • ??分析
_onChange=(event)=> {
        this.setState({
            text: event.nativeEvent.text,
        });
 }
_onContentSizeChange=(event)=> {
        this.setState({
            height: event.nativeEvent.contentSize.height
        });
}
render() {
        return (
            <TextInput  {...this.props}
                multiline={true}
                onChange={this.onChange}
                onContentSizeChange={this.onContentSizeChange}
                style={[styles.textInputStyle, {height: Math.max(35, this.state.height)}]}
                value={this.state.text}/>
        );
    }
}

  • 結(jié)果分析:當(dāng)檢測(cè)到文本框內(nèi)容布局變化時(shí),我們便會(huì)將獲取到的高度置給TextInput組件,該方法已經(jīng)兼容了刪除時(shí)的操作

react native to web的實(shí)現(xiàn)方案

  • rn-to-web中只實(shí)現(xiàn)了高度變化時(shí)會(huì)觸發(fā)onContentSizeChange,但是沒有實(shí)現(xiàn)內(nèi)部的邏輯event.nativeEvent屬性是不存在的,所以我們要通過類似方案二的解決辦法,獲取到原生的scrollHeight屬性
  • ??分析
render() {
        return (
            <TextInput  {...this.props}
                multiline={true}
                onChange={this.onChange}
                onContentSizeChange={event => {
                  const node = this.input._node
                if (node) {
                    node.style.height = 'inherit'
                    const height = node.scrollHeight
                    node.style.height = `${height}px`
                    this.setState({ height })
                  }
               }}
                style={[styles.textInputStyle, {height: Math.max(35, this.state.height)}]}
                value={this.state.text}/>
        );
    }

  • 結(jié)果分析:this.input._node獲取到的是原生的dom節(jié)點(diǎn),因此里面采用和是方案二同樣的處理方式,
  • 基本原理和方案二相同,但是由于onContentSizeChange的觸發(fā)時(shí)機(jī)本就是在高度變化的時(shí)候,所以react nativereact native to web的這兩種實(shí)現(xiàn)方式均不需要進(jìn)行優(yōu)化處理

源碼參考:

https://github.com/yyn7/study-demo/tree/master/1.%E9%AB%98%E5%BA%A6%E8%87%AA%E9%80%82%E5%BA%94%E7%9A%84%E8%BE%93%E5%85%A5%E6%A1%86

寫在最后:

第一篇分享文章,感覺寫的略有點(diǎn)啰嗦,比較適合新手閱讀,后面會(huì)逐漸改進(jìn),大家有什么想法和優(yōu)化思路歡迎來交流啊,一起努力成為合格的前端工程師!
最后的最后 附上wuli超級(jí)無敵可愛的琪琪

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容