[貝聊科技]不簡單的自適應高度輸入框

做者:韓永豪 前端開發部 前端開發工程師javascript

前言

前段時間在項目開發中遇到這樣一個需求——文本輸入框的高度要隨着框內文本所佔高度而變化。css

自增高輸入框

下面講一下實現方案的探索過程。html

方案一:利用contenteditable屬性

首先想到的方法,是使用 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放置於一個設置了「position: relative」樣式的容器中;
  • 給textarea設置「position: absolute」,並把寬高設爲100%;
  • 使textarea與佔位容器的文本樣式一致;
  • textarea內容變化時(監聽input事件),把內容同步到佔位容器。

這樣一來,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的scrollHeight

開發太小程序的朋友應該知道,小程序的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;
}
複製代碼

雖然功能是作出來了,可是性能上還有優化的餘地。由於當前的作法,至關於每次輸入都要同步高度。若是高度沒有發生變化,這個同步操做是沒有意義的。因此,優化的思路就在於如何檢查內容高度是否發生了變化:

  • 內容增長時,scrollHeight有可能會發生變化,因此能夠記錄上一次的scrollHeight,並與當前的scrollHeight對比,有變化時才設置高度樣式。
  • 內容減小時,沒有有效的方式能夠知道內容高度是否有變動(scrollHeight不會減小),因此這種狀況目前沒法優化。

實現代碼以下:

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;
});
複製代碼

這就是最終的實現方案。

相關文章
相關標籤/搜索