高度自适应的输入框

  有时候我们需要一个高度能随内容自动增加的输入框,input 显然不行,因为 input 里的文字是不换行的。文本域 textarea 里的文字倒是换行的,可一旦文字内容超过其高度,textarea 就会增加一个烦人的滚动条,这是很影响视觉的,就如同下面:

<textarea cols="30" rows="3"></textarea>

  那么有没有办法制作一个高度能随文字内容自动增加的输入框呢?答案是肯定的,下面介绍两种方式。

方式一

  这种方式依然使用 textarea, 主要思想是我们将 textarea 放入一个容器中,同时在这个容器中放入一个隐藏的 div (visibility: hidden), 监听 textarea 的输入事件并将其中的文字动态的同步到隐藏的div中,这样div 就可以撑开容器,这时设置 textarea 的高度为 100% 并将其定位到容器的左上角,那么 textarea 的高度自然就是其中文字内容的高度了。

visibility 是一个CSS属性,用来在不更改文档的布局的前提下显示或隐藏元素,它有三个可能的取值:

  1. visible 元素正常显示(默认值);
  2. hidden 隐藏元素,但是其他元素的布局不改变,相当于此元素变成透明。
      若将其子元素设为 visibility: visible,则该子元素依然可见;
  3. collapse 用于表格的行、列、列组和行组,隐藏表格的行或列,并且不占用任何空间。
   <!-- demo-1.html -->

   <div class="container">
      <div></div>
      <textarea placeholder="输入消息..."></textarea> 
   </div>
   
   <style>
      .container {
         width: 500px;
         position: relative;
      }
      .container div {         
         visibility: hidden;
         /** 避免初始化时容器没有高度 */
         padding: 8px 0px;
      }
      .container textarea {
         width: 100%;
         height: 100%;
         position: absolute;
         top: 0px;
         padding: 0px;
         /** 必须设置为 content-box !!! */ 
         box-sizing: content-box;
      }
   </style>

   <script>
      const textarea = document.querySelector('.container textarea');
      const div = document.querySelector('.container div');
      textarea.addEventListener('input', (e) => {
         div.innerText = e.target.value;
      });
   </script>

查看样例:https://mxsyx.site/archives/10/#demo-1

  你可能已经注意到了,当我们输入文字时,输入框的高度显然要比文字内容高许多,伴随着输入文字的增多。高度差会越来越大,这是因为隐藏 div 与 文本域 textarea 内字体的尺寸与行高是不同的, div 内的字体尺寸与行高要比 textarea 内的大,所以 div 撑开的容器高度自然要高于 textarea 内的文字内容高度。要解决这个问题,统一它们的字体尺寸与行高就可以了。(注:div 的字体尺寸与行高默认继承自父元素)

   <!-- demo-1.html -->

   <div class="container">
      <div></div>
      <textarea placeholder="输入消息..."></textarea> 
   </div>

   <style>
      .container {
         width: 500px;
         position: relative;
         font-size: 14px;
         line-height: 16px;
      }
      .container div {
         visibility: hidden;
         /** 避免初始化时容器没有高度 */
         padding: 8px 0px;
      }
      .container textarea {
         width: 100%;
         height: 100%;
         position: absolute;
         top: 0px;
         padding: 0px;
         /** 必须设置为 content-box !!! */
         box-sizing: content-box;
         /** 设置字体尺寸与行高继承自父元素 */
         font-size: inherit;
         line-height: inherit;
         /* 去掉右下角的调整大小的标志 */
         resize: none;
      }
   </style>
   <script>
      const textarea = document.querySelector('.container textarea');
      const div = document.querySelector('.container div');
      textarea.addEventListener('input', (e) => {
         div.innerText = e.target.value;
      });
   </script>

查看样例:https://mxsyx.site/archives/10/#demo-2

  这样一来高度就一致了。这种方式虽然可以较好的实现高度自适应的输入框,但实现起来总感觉很粗糙,下面这种方式就明显简单多了。

方式二

  像 div, p, blockquote 这样的元素默认是不可编辑的,但我们可以将其 contenteditable 属性设置为 true, 使其变为可编辑的。

contenteditable是一个全局属性,用于指示元素是否可被用户编辑,该属性必须采用以下值之一:

  1. true 或者 '空字符串', 表示该元素是可编辑的;
  2. false, 表示该元素是不可编辑的。
  3. 如果未设置此属性,则其默认值将从其父元素继承。
  <div class="container" contenteditable="true"></div>

  <style>
    .container {
      width: 500px;
      font-size: 14px;
      line-height: 16px;
      border: solid 1px #999;
    }
  </style>

尝试输入一段文字吧:

  是不是很简单呢? 我们也可以使用CSS伪类 :empty, :focus, 实现placeholder 那样的效果

<style>
.container {
  width: 500px;
  font-size: 14px;
  line-height: 16px;
  border: solid 1px #999;
}
.container:empty::before {
  content: "输入消息...";
  color: #999999;
}
.container:focus::before {
  content: none;
}
</style>

尝试输入一段文字吧:

  如果你使用 Vue.js, 我们也可以它封装为一个Vue组件:

<template>
    <div
      class="msg-input"
      contenteditable="true"
      @input="changeText"
    >{{ innerText }}</div>
</template>

<script>
export default{
  name: "MsgInput",
  props: ['value'],
  
  data: function() {
    return {
      innerText: this.value,
    }
  },
  
  methods: {
    changeText() {
      this.$emit('input', this.$el.innerText);
    }
  }
}
</script>

<style scoped>
.msg-input {
  width: 500px;
  font-size: 14px;
  line-height: 16px;
  border: solid 1px #999;
}
.msg-input:empty::before {
  content: "输入消息...";
  color: #999999;
}
.msg-input:focus::before {
  content: none;
}
</style>

接下来在父组件中引用这个组件:

<template>
  <div>
    <MsgInput v-model="msg"/>
  </div>
</template>

<script>
import MsgInput from "/MsgInput.vue";

export default {
  name: "MsgToolkit",
  data: function() {
    return {
      msg: ''
    }
  },

  components: {
    MsgInput
  },
}
</script>

  父组件为子组件使用 v-model 指令,将子组件的 value 与 父组件的 msg 双向绑定在一起。当输入事件发生后,子组件调用changeText方法,触发一个 input 事件,父组件监听到此事件,将事件传递过来的数据同步到 msg 上,由于数据是双向绑定的,子组件的 value 值也会相应发生变化。更过原理请参考 自定义组件的-v-model

该篇博客内的代码已同步到Github

参考资料:
[1]. MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/contenteditable
[2]. MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/CSS/visibility
[3]. MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/CSS/:empty
[4]. MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/CSS/:focus
[5]. Vue.js官方文档 https://cn.vuejs.org/v2/guide/components-custom-events.html#自定义组件的-v-model

posted @ 2019-12-13 15:27  梦醒时夜续  阅读(4268)  评论(0编辑  收藏  举报