“Yeah It’s on. ”
正文
需求:文本输入框的高度要随着框内文本所占高度而变化
方案一:利用contenteditable属性
首先想到的方法,是使用 HTML 5 中新增的contenteditable属性。它可以把元素变成可编辑状态,同时让其保留原有的特性(如元素高度根据元素内容所占高度而变化)。使用方式如下:
<element contenteditable="true">
带有contenteditable属性的元素可以当作富文本编辑器,默认支持粘贴带格式(样式)的HTML代码。如果要限制输入框只能输入纯文本内容,这里可以把user-modify样式设为「read-write-plaintext-only」,或者把contenteditable属性的值设为「plaintext-only」。具体写法如下:
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;
});
这就是最终的实现方案。
作者:贝聊科技 链接:https://juejin.im/post/5b7653bde51d454dba70c0b1 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。