前言
业务开发中,经常会遇到文本输入框高度随着输入内容高度变化的情况,下面我们来详细说明一下实现这种输入框的方案和解题思路
方案一为一种扩展思路,仅供参考
方案二为常规思路,急着用的小伙伴建议直接看二
第三个板块 附上了react native和react native to web的代码实现方案
※方案一:contenteditable属性法
- contenteditable属性表示元素是否可编辑,变为可编辑状态的元素还保留其原有的特性,属性值为如下两者之一
-true或空字符串,表示元素是可编辑的;
-false表示元素不是可编辑的; - 该属性是一个枚举属性,而非布尔属性。这意味着必须显式设置其值为
true
、false
或空字符串中的一个,最好不要简写为<label contenteditable>Example Label</label>
- 正确的用法是 :
<element contenteditable="value"> --value=true/false
- 🌰分析
<style type="text/css">
.container {
padding: 20px;
}
.auto-input {
min-height: 100px;
font-size: 30px;
border: 1px solid red;
}
</style>
<body>
<h1>contenteditable实现的高度自适应输入框</h1>
<div class="container">
<div class="auto-input" contenteditable="true" id="auto-input"></div>
</div>
</body>
- 注意事项:如想要设置文本输入框的默认高度,设置
min-height
即可,文本输入框同时支持focus,blur
事件,但是即使外表伪装的和textarea一模一样,还是有需要注意的坑点,试着分别输入和复制内容,查看dom节点的变化:
以下为手打内容和复制黏贴的区别
可以看出内容其实并不是真正的纯文本,而是带有样式的富文本格式,黏贴进去的内容呈现的还是复制前的样式,我们可以通过innerText获取到里面的纯文本内容
保留此功能很适合展示图片,复制一张图片进入输入框中可直接展示
- 纯文本设置方法:为了完成和
textarea
同样的作用,我们可以在输入时进行过滤,保证输入的是纯文本文件,有两种办法
- 在css中设置
div[contenteditable] {
user-modify: read-write-plaintext-only
}
user-modify 可以控制普通元素是否可读写
user-modify: read-only; // 只读
user-modify: read-write; // 可读写,支持富文本
user-modify: write-only; // 只写,支持的浏览器很少
user-modify: read-write-plaintext-only;//可读写,纯文本,目前只有webkit内核浏览器支持
- 在html中给div增加属性
<div contenteditable="plaintext-only">
- 结果分析:
这种方案的缺点在于,一个元素加上contenteditable
,即使解决了可编辑的问题,但是表单控件的一些特性placeholder
,maxlength
,autofocus
,只能js去辅助完成,在移动端会有一些兼容性的问题
方案二:纯textarea文本框实现
思路初始化—创建textarea元素
- 我们预期通过一个文本输入框textarea即可完成高度自适应,但是实际表明,如果只是通过textarea和一些简单的css方法,设置textarea的
min-height
后会发现,当输入的元素内容的高度超过设置的最小高度时,会产生滚动条,显然不符合我们的预期
进展1—获取元素的scrollTop
和offsetHeight
并设置高度
- 既然产生了滚动条,那就可以尝试着获取文本框的
scrollTop
,加上文本框的原有高度,在监听到文本框内容改变的时候重新设置文本框的高度 - 我们用
offsetHeight
来获取文本框的高度,该属性包含文本框的border + padding + content
的高度,因此我们要在css中将textarea设置为border-box
,方便设置的时候统一 - 最后元素高度的设置后只是相当于增加了scrollTop部分的值
- 实现步骤如下:
- input监听文本框内容的改变
- 获取文本框的滚动距离
scrollTop
- 获取文本框的高度
offsetHeight
- 设置文本框的新
height
为scrollTop+offsetHeight
- 🌰分析:
css:
<style>
body,
html {
padding-left: 0.1rem;
margin: 0;
}
.auto-input {
display: block;
box-sizing: border-box;
outline: none;
resize: none;
padding: 0;
width: 2rem;
height: 30px;
border: 1px solid #000;
font-size: 0.2rem;
}
.title {
font-size: 0.2rem;
}
</style>
html:
<body>
<h1 class="title">textarea js高度自适应输入框</h1>
<textarea class="auto-input" id="autoInput"></textarea>
</body>
script:
<script>
var autoInput = document.getElementById("autoInput");
autoInput.addEventListener("input", function() {
var inputScrollTop = autoInput.scrollTop;
var inputHeight = autoInput.offsetHeight;
console.log("inputScrollTop:" + inputScrollTop + 'px')
autoInput.style.height = inputScrollTop + inputHeight + "px";
});
</script>
- 测试结果如下:
- 结果分析: 在输入到下一行的时候,第一个导致换行的字符在触发input事件的时候获取到的
scrollTop
的值并未改变,仍然为旧值,直到输入新一行的第二个字符的时候才有所响应,猜测原因是scrollTop
的获取时机太早导致的问题
进展2—增加定时器延缓执行
- 我们尝试在js中获取
scrollTop
时外加入定时器,延缓获取时机
var autoInput = document.getElementById("autoInput");
autoInput.addEventListener("input", function() {
setTimeout(()=>{
var inputScrollTop = autoInput.scrollTop;
var inputHeight = autoInput.offsetHeight;
console.log("inputScrollTop:"+inputScrollTop+'px');
autoInput.style.height = inputScrollTop + inputHeight + "px";
},0)
});
- 测试结果如下:
- 结果分析:确实如预期,添加定时器延缓了获取scrollTop的时机后,在换行时获取到的
scrollTop
为准确的。
进展3—获取元素的scrollHeight
并设置高度
- 虽然在开发过程中定时器确实能解决很多头疼的问题,但是个人觉得不是很优雅,这里我们尝试着去获取另一个属性,
scrollHeight
,对于滚动元素来说scrollHeight
代表的是元素原有的高度加上内容滚动到底部时的scrollTop
,换句话说也就是元素的完整内容高度,这个属性包含元素的padding
,不包含border
和margin
- 需要注意获取的
scrollHeight
本身是不包含边框的高度的,但是我们要重置的height
,因为设置为border-box
,是包含边框的,因此需要将scollHeight
加上边框后再设置给textarea的height
- 实现步骤如下:
- 监听元素变化时我们去获取
scrollHeight
- 设置
scrollHeight+border
为元素高度
- 监听元素变化时我们去获取
- 🌰分析:
script:
autoInput.addEventListener("input", function() {
var inputScrollHeight = autoInput.scrollHeight + 2;
console.log("inputScrollHeight" + inputScrollHeight+"px");
autoInput.style.height = autoInput.scrollHeight + "px";
});
- 测试结果:
- 结果分析:当输入元素内容的时候,确实可以如我们预期的,随着内容的增加高度增加,但是删除的时候表现却发现textarea的高度没有变化,
scrollHeight
是用来获取元素滚动的scrollTop,padding
,以及内容的高度的,那么当删除文本内容时它是否会发生变化呢?从上面视频打印的结果来看删除的时候元素的高度并未发生变化
原因如下:如果我们没有给文本框设置高度,随着内容的增加
scrollHeight = scrollMaxTop+clientHeight;//元素高度加滚动最大距离
其中scrollTop
会随着内容增加可滚动的距离变大而增加,所以在添加文字的情况下我们可以发现scrollHeight
会不断增大
我们将盒子本身的高度设置成scrollHeight
,
newClientHeight = scrollHeight
删除的情况下,盒子高度足够是没有滚动距离的,因此scrollMaxTop为0,newClientHeight不会再更新,因此盒子也就维持了之前的高度
scrollHeight = 0 + newClientHeight
进展4—重置元素的高度
- 上面因为删除时
scrollHeight
并不会变化导致元素的高度维持在了之前的最大值,那么我们如果在删除元素时,将元素的高度设置成根据内容自适应(auto)/("")
,这样textarea的高度会被重置成最小化 - 最小化之后重新获取到的
scrollHeight
,又是可以让当前内容自适应的高度 - 需要注意auto和""两者的区别 如果设置为auto的话 textarea的高度会被重置为默认高度,默认高度不是指css中设置的高度,而是浏览器默认的,但是如果设置为(“”)那么相当于清楚的是内联的高度样式,并不会覆盖css的高度,textarea本身的css高度还是存在的,因此表现不同点在于textarea最小时的高度,所以这里建议使用"",可以保留原本设置的高度,但是如果原本设置的是textarea的
min-height
而不是height,那两个属性均可 - 实现步骤如下:
- 监听元素变化时我们将元素的height设置为““,目的是为了清楚上一次的高度
- 重新获取元素的
scrollHeight
- 设置元素的高度为
scrollHeight+border
- 🌰分析
autoInput.addEventListener("input", function() {
autoInput.style.height = "";
//注意顺序 需要先重置 再获取
var inputScrollHeight = autoInput.scrollHeight + 2;
console.log("inputScrollHeight" + inputScrollHeight+"px");
autoInput.style.height = inputScrollHeight + "px";
});
- 测试结果:
- 结果分析:到此我们终于完成了输入框的基本功能。但是现在每次Input监听时,我们都会将元素的高度重置为空,并且每次都会获取
scrollHeight
的高度,无疑会对性能有一些损耗,因此我们后面会尝试一下优化方案。
进展5—优化方案
- 我们尝试增加一些判断条件来减少不必要的执行
- 在元素内容增多的时候,我们期望只有当元素内容换行的时候才进行重置操作,但是怎么去监听元素的换行呢,我们可以通过获取到的
scrollHeight
,当该值增加的时候再去进行设置高度的操作 - 另一方面 当元素内容减少的时候我们才需要将元素的高度置空,是否也可以通过判断
scrollHeight
的值是否变小才进行这种判断呢,答案无疑是否定的,因为如果不去设置高度为空的话,scrolllHeight
的值并不会发生变化,目前想到的判断字符减少的方案为监听输入的字符个数 - 如果字符数减少的话我们需要将元素的height置为空,然后重新获取元素的
scrollHeight
,- 如果减少的字符导致了换行,那么
scrollHeight
的值会发生变化 - 如果减少的字符没有导致换行,那么
scrollHeight
没有发生变化
- 如果减少的字符导致了换行,那么
- 无论哪种情况我们都需要去把
scrollHeight
的值赋值给textarea的height,否则会变为css中设置的最小高度 - 因此textarea高度重新设置的的条件为
scrollHeight
增加导致换行或者文字内容减少 - 实现步骤如下:
- 我们先获取textarea原本的
scrollHeight
和字符串长度 - 在监听到内容改变的时候,我们获取一下新内容的长度
- 如果长度变小 重置文本框的高度
- 然后获取文本框的
scrollHeight
- 如果
scrollHeight
变大或者元素的height不存在进入到if判断中 - 将之前存储的文本框的高度设置成新的方便下一次比较
- 设置文本框的高度为新获取到的
scrollHeight
+边框 结束if语句 - 最后重置下文本框的新内容的长度
- 我们先获取textarea原本的
- 🌰分析
var lastScrollHeight = autoInput.scrollHeight;
var lastTextLength = autoInput.value.length;
autoInput.addEventListener("input", function() {
var inputTextLength = autoInput.value.length;
if (inputTextLength < lastTextLength){
autoInput.style.height = "";
}
//注意这句话一定要写在设置height为空的后面,否则获取不到最新的scrollHeight
var inputScrollHeight = autoInput.scrollHeight;
//注意如果height为空的话也需要重置高度 否则高度有问题
if(lastScrollHeight < inputScrollHeight || !autoInput.style.height){
lastScrollHeight = inputScrollHeight;
autoInput.style.height = inputScrollHeight + 2 + "px";
}
lastTextLength = autoInput.value.length;
});
- 测试结果:同上
- 结果分析:至此已经完成了比较完善的自适应输入框
react native和rn2web的实现方法
RN的实现方案
- rn方法提供
onContentSizeChange
的函数,onContentSizeChange
是在内容布局改变(如换行)的时候能获取到当前contentSize
中的高度,然后通过state调整为input的高度 - 🌰分析
_onChange=(event)=> {
this.setState({
text: event.nativeEvent.text,
});
}
_onContentSizeChange=(event)=> {
this.setState({
height: event.nativeEvent.contentSize.height
});
}
render() {
return (
<TextInput {...this.props}
multiline={true}
onChange={this.onChange}
onContentSizeChange={this.onContentSizeChange}
style={[styles.textInputStyle, {height: Math.max(35, this.state.height)}]}
value={this.state.text}/>
);
}
}
- 结果分析:当检测到文本框内容布局变化时,我们便会将获取到的高度置给
TextInput
组件,该方法已经兼容了删除时的操作
react native to web的实现方案
- rn-to-web中只实现了高度变化时会触发
onContentSizeChange
,但是没有实现内部的逻辑event.nativeEvent
属性是不存在的,所以我们要通过类似方案二的解决办法,获取到原生的scrollHeight
属性 - 🌰分析
render() {
return (
<TextInput {...this.props}
multiline={true}
onChange={this.onChange}
onContentSizeChange={event => {
const node = this.input._node
if (node) {
node.style.height = 'inherit'
const height = node.scrollHeight
node.style.height = `${height}px`
this.setState({ height })
}
}}
style={[styles.textInputStyle, {height: Math.max(35, this.state.height)}]}
value={this.state.text}/>
);
}
- 结果分析:
this.input._node
获取到的是原生的dom节点,因此里面采用和是方案二同样的处理方式, - 基本原理和方案二相同,但是由于
onContentSizeChange
的触发时机本就是在高度变化的时候,所以react native
和react native to web
的这两种实现方式均不需要进行优化处理
源码参考:
写在最后:
第一篇分享文章,感觉写的略有点啰嗦,比较适合新手阅读,后面会逐渐改进,大家有什么想法和优化思路欢迎来交流啊,一起努力成为合格的前端工程师!
最后的最后 附上wuli超级无敌可爱的琪琪