做者:韓永豪 前端開發部 前端開發工程師javascript
前段時間在項目開發中遇到這樣一個需求——文本輸入框的高度要隨着框內文本所佔高度而變化。css
下面講一下實現方案的探索過程。html
首先想到的方法,是使用 HTML 5 中新增的contenteditable屬性。它能夠把元素變成可編輯狀態,同時讓其保留原有的特性(如元素高度根據元素內容所佔高度而變化)。使用方式以下:前端
<element contenteditable="value">
複製代碼
帶有contenteditable屬性的元素能夠看成富文本編輯器,默認支持粘貼帶格式(樣式)的HTML代碼。若是要限制輸入框只能輸入純文本內容,這裏能夠把user-modify樣式設爲「read-write-plaintext-only」,或者把contenteditable屬性的值設爲「plaintext-only」。具體寫法以下:java
element[contenteditable] {
user-modify: read-write-plaintext-only
}
複製代碼
或者:小程序
<element contenteditable="plaintext-only">
複製代碼
這種方案的缺點在於,一個div加上contenteditable屬性後也並不能讓其支持如placeholder、maxlength等表單控件特性,只能經過額外的JavaScript代碼去實現。而咱們在實踐中發現,這樣會致使很多兼容上的問題。緩存
因爲方案一存在很多兼容上的問題,因此在近期項目優化中探索出一種基於原生textarea的實現方案:異步
這樣一來,textarea的內容高度變化時,佔位容器的高度也會變化,從而使外層容器的高度也產生變化。textarea與外層容器尺寸一致,因此也會同步變化。編輯器
初步實現後代碼以下:工具
<div class="container">
<!-- 佔位容器 -->
<span id="text" class="text font-style"></span>
<!-- 輸入框 -->
<textarea id="textarea" class="textarea font-style"></textarea>
</div>
複製代碼
.container {
position: relative;
min-height: 90px;
}
.text {
font-size: 0;
color: transparent;
}
.textarea {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
resize: none;
border: 0;
outline: none;
}
/* 統一內容樣式 */
.font-style {
font-family: Helvetica;
word-wrap: break-word;
word-break: break-all;
line-height: 48px;
font-size: 32px;
}
複製代碼
var $text = document.getElementById('text');
var $textarea = document.getElementById('textarea');
$textarea.addEventListener('input', function(e) {
$text.innerText = e.target.value;
});
複製代碼
實現後發現,textarea的換行符是「\n」,與HTML的(<br />)不一致,致使內容同步到容器中後丟失了換行。
這能夠經過設置CSS樣式「white-space」來解決:
.text {
white-space: pre-wrap;
}
複製代碼
此外,若是換行符恰好在內容的末尾,那麼在容器中的換行並不會生效,這時候須要在換行符後面補上一個空格(或者其餘非空白字符)。
$textarea.addEventListener('input', function(e) {
$text.innerText = e.target.value.replace(/\n$/, '\n '); // 解決不換行問題
});
複製代碼
注意:若是在Vue.js中使用該方案,在內容同步的過程當中,不能直接使用v-model實現。
因爲Vue.js在數據變化時,並非同步更新到界面中,而是會把當前操做所發生的數據變化緩存到一個隊列中,按順序異步執行每個「tick」。
這意味着,在textarea輸入內容後,佔位容器的內容在短期內(幾毫秒)還維持着上一個狀態。當內容換行時,容器的高度不足以讓textarea顯示完整的內容,會出現跳一下的現象。
所以,內容同步只能經過原生節點操做實現。
<div class="container">
<!-- 佔位容器 -->
<span class="font-style" ref="text"></span>
<!-- 輸入框 -->
<textarea class="textarea font-style" v-model="resultValue" @input="inputHandler"></textarea>
</div>
複製代碼
{
methods: {
inputHandler() {
let $text = this.$refs.text;
if ($text) {
$text.innerText = this.data.resultValue.replace(/\n$/, '\n ');
}
}
}
}
複製代碼
最終效果以下:
開發太小程序的朋友應該知道,小程序的textarea控件也能夠自適應高度。可是,從開發者工具能夠發現,它並無經過佔位容器去實現,僅僅是一個textarea就能夠實現這個功能。
查閱文檔後發現有這樣一個節點屬性「scrollHeight」,對於一個內部可滾動的元素來講,它表示元素中完整內容的高度(注意:「scrollHeight」包括元素的padding,但不包括元素的border和margin。)。
那麼,只須要在textarea的內容變化後,把它的高度設爲它的「scrollHeight」,就能夠完成自適應高度。
初步實現代碼以下:
textarea {
width: 100%;
height: 92px;
padding: 20px;
line-height: 50px;
resize: none;
outline: none;
border: 1px solid #ccc;
background: #eee;
font-size: 32px;
box-sizing: border-box;
}
複製代碼
<textarea id="textarea"></textarea>
複製代碼
var $textarea = document.getElementById('textarea');
$textarea.addEventListener('input', function() {
// 總高度 = scrollHeight + 上下邊框的寬度(1px * 2)
$textarea.style.height = $textarea.scrollHeight + 2 + 'px';
});
複製代碼
然而,當內容高度縮減時,輸入框的高度並無跟隨縮減。
因爲根據scrollHeight設置的元素高度的存在,即便內容高度縮減,此時scrollHeight也不會低於元素高度。因此,在作自適應高度縮減時就沒法直接經過同步scrollHeight來實現,而是要先清掉高度樣式:
$textarea.addEventListener('input', function() {
// 清除原來高度
$textarea.style.height = '';
$textarea.style.height = $textarea.scrollHeight + 2 + 'px';
});
複製代碼
實現後發現,輸入到臨近換行處,內容高度提早增高了。
調試後發現,清掉高度樣式後,textarea恢復到原來的高度,此時內容超過textarea高度,所以會出現滾動條。滾動條會佔據必定的空間,致使一行能容納的字符減小,因而就提早換行了(以下圖所示)。而由於在清理高度樣式後,又馬上把高度設爲新的scrollHeight,因此在界面上沒有體現出現。
要解決這個問題,只須要把滾動條隱藏掉。
textarea {
overflow: hidden;
}
複製代碼
雖然功能是作出來了,可是性能上還有優化的餘地。由於當前的作法,至關於每次輸入都要同步高度。若是高度沒有發生變化,這個同步操做是沒有意義的。因此,優化的思路就在於如何檢查內容高度是否發生了變化:
實現代碼以下:
var $textarea = document.getElementById('textarea');
var lastLength = 0;
var lastHeight = 0;
$textarea.addEventListener('input', function() {
var currentLength = $textarea.value.length;
// 判斷字數若是比以前少了,說明內容正在減小,須要清除高度樣式,從新獲取
if (currentLength < lastLength) {
$textarea.style.height = '';
}
var currentHeight = $textarea.scrollHeight;
// 若是內容高度發生了變化,再去設置高度值
if (lastHeight !== currentHeight || !$textarea.style.height) {
$textarea.style.height = currentHeight + 2 + 'px';
}
lastLength = currentLength;
lastHeight = currentHeight;
});
複製代碼
這就是最終的實現方案。