做者:韩永豪 前端开发部 前端开发工程师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;
});
复制代码
这就是最终的实现方案。