高度自适应输入框的清晰解题思路

*爱你&永不变心* 提交于 2020-01-19 20:53:19

前言

业务开发中,经常会遇到文本输入框高度随着输入内容高度变化的情况,下面我们来详细说明一下实现这种输入框的方案和解题思路

方案一为一种扩展思路,仅供参考
方案二为常规思路,急着用的小伙伴建议直接看二
第三个板块 附上了react native和react native to web的代码实现方案

※方案一:contenteditable属性法

  • contenteditable属性表示元素是否可编辑,变为可编辑状态的元素还保留其原有的特性,属性值为如下两者之一
    -true或空字符串,表示元素是可编辑的;
    -false表示元素不是可编辑的;
  • 该属性是一个枚举属性,而非布尔属性。这意味着必须显式设置其值为 truefalse 或空字符串中的一个,最好不要简写为<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同样的作用,我们可以在输入时进行过滤,保证输入的是纯文本文件,有两种办法
  1. 在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内核浏览器支持      
  1. 在html中给div增加属性
<div contenteditable="plaintext-only">
  • 结果分析:
    这种方案的缺点在于,一个元素加上contenteditable,即使解决了可编辑的问题,但是表单控件的一些特性placeholdermaxlengthautofocus,只能js去辅助完成,在移动端会有一些兼容性的问题

方案二:纯textarea文本框实现

思路初始化—创建textarea元素

  • 我们预期通过一个文本输入框textarea即可完成高度自适应,但是实际表明,如果只是通过textarea和一些简单的css方法,设置textarea的min-height后会发现,当输入的元素内容的高度超过设置的最小高度时,会产生滚动条,显然不符合我们的预期

进展1—获取元素的scrollTopoffsetHeight并设置高度

  • 既然产生了滚动条,那就可以尝试着获取文本框的scrollTop,加上文本框的原有高度,在监听到文本框内容改变的时候重新设置文本框的高度
  • 我们用offsetHeight来获取文本框的高度,该属性包含文本框的border + padding + content的高度,因此我们要在css中将textarea设置为border-box,方便设置的时候统一
  • 最后元素高度的设置后只是相当于增加了scrollTop部分的值
  • 实现步骤如下:
    1. input监听文本框内容的改变
    2. 获取文本框的滚动距离scrollTop
    3. 获取文本框的高度offsetHeight
    4. 设置文本框的新heightscrollTop+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,不包含bordermargin
  • 需要注意获取的scrollHeight本身是不包含边框的高度的,但是我们要重置的height,因为设置为border-box,是包含边框的,因此需要将scollHeight加上边框后再设置给textarea的height
  • 实现步骤如下:
    1. 监听元素变化时我们去获取scrollHeight
    2. 设置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,那两个属性均可
  • 实现步骤如下:
    1. 监听元素变化时我们将元素的height设置为““,目的是为了清楚上一次的高度
    2. 重新获取元素的scrollHeight
    3. 设置元素的高度为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增加导致换行或者文字内容减少
  • 实现步骤如下:
    1. 我们先获取textarea原本的scrollHeight和字符串长度
    2. 在监听到内容改变的时候,我们获取一下新内容的长度
    3. 如果长度变小 重置文本框的高度
    4. 然后获取文本框的scrollHeight
    5. 如果scrollHeight变大或者元素的height不存在进入到if判断中
    6. 将之前存储的文本框的高度设置成新的方便下一次比较
    7. 设置文本框的高度为新获取到的scrollHeight+边框 结束if语句
    8. 最后重置下文本框的新内容的长度
  • 🌰分析
  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 nativereact native to web的这两种实现方式均不需要进行优化处理

源码参考:

https://github.com/yyn7/study-demo/tree/master/1.%E9%AB%98%E5%BA%A6%E8%87%AA%E9%80%82%E5%BA%94%E7%9A%84%E8%BE%93%E5%85%A5%E6%A1%86

写在最后:

第一篇分享文章,感觉写的略有点啰嗦,比较适合新手阅读,后面会逐渐改进,大家有什么想法和优化思路欢迎来交流啊,一起努力成为合格的前端工程师!
最后的最后 附上wuli超级无敌可爱的琪琪

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!