[贝聊科技]不简单的自适应高度输入框

做者:韩永豪 前端开发部 前端开发工程师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;
});
复制代码

这就是最终的实现方案。

相关文章
相关标签/搜索