vue-textarea 自适应高度

需求简介

一个搜索页面,上面输入框,下面列表展示搜索到的结果。

重点是:产品要求搜索框默认显示一行,当输入的文字超过一行时,输入框的高度会随着改变,直到输入完毕。

解决思路设想

本想利用textarea实现,但textarea不支持自适应高度,而是定好高度或者是行数之后,超出部分就会显示滚动条。只能另想。

根据需求,首先想到了张鑫旭伪类匹配列表数目实现微信群头像CSS布局的技巧一文提到的文字多字号自动变小的技巧,但仔细一琢磨,不行。这个是根据内容元素的个数,进行处理,而这儿是输入框,没有内容元素。

后面想到可以利用html属性contenteditable="true",加在div上让其可编辑来模拟自适应高度。可是需要在vue中双向绑定实现,这个不是很好处理。

后面想到利用textarea的row属性,根据输入内容的长度控制row的值,为1-n行,但这个似乎不是很智能,因为多少个字体一行不一定,英文、中文、数字的宽度不一致,而且row属性在每个浏览器中的表现不一致。

最后利用textarea,监听change事件,让其高度=其滚动条高度,来达到高度自适应。

没想到最后还是利用了textarea。

实现

参考:textarea如何实现高度自适应?

util.js

/**
* 文本框根据输入内容自适应高度
* @param       {HTMLElement}   输入框元素
* @param       {Number}        设置光标与输入框保持的距离(默认0)
* @param       {Number}        设置最大高度(可选)
* @callback    {Function}      设置回调函数(可选)
*/
export const autoTextarea = function (elem, extra, maxHeight, callback) {
  extra = extra || 0;
  var isFirefox = !!document.getBoxObjectFor || 'mozInnerScreenX' in window,
    isOpera = !!window.opera && !!window.opera.toString().indexOf('Opera'),
    addEvent = function (type, callback) {
      elem.addEventListener ?
        elem.addEventListener(type, callback, false) :
        elem.attachEvent('on' + type, callback);
    },
    getStyle = elem.currentStyle ? function (name) {
      var val = elem.currentStyle[name];

      if (name === 'height' && val.search(/px/i) !== 1) {
        var rect = elem.getBoundingClientRect();
        return rect.bottom - rect.top -
          parseFloat(getStyle('paddingTop')) -
          parseFloat(getStyle('paddingBottom')) + 'px';
      };

      return val;
    } : function (name) {
      return getComputedStyle(elem, null)[name];
    },
    minHeight = parseFloat(getStyle('height'));

  elem.style.resize = 'none';

  var change = function () {
    var scrollTop, height,
      padding = 0,
      style = elem.style;

    if (elem._length === elem.value.length) return;
    elem._length = elem.value.length;

    if (!isFirefox && !isOpera) {
      padding = parseInt(getStyle('paddingTop')) + parseInt(getStyle('paddingBottom'));
    };
    scrollTop = document.body.scrollTop || document.documentElement.scrollTop;

    elem.style.height = minHeight + 'px';
    if (elem.scrollHeight > minHeight) {
      if (maxHeight && elem.scrollHeight > maxHeight) {
        height = maxHeight - padding;
        style.overflowY = 'auto';
      } else {
        height = elem.scrollHeight - padding;
        style.overflowY = 'hidden';
      };
      style.height = height + extra + 'px';
      scrollTop += parseInt(style.height) - elem.currHeight;
      document.body.scrollTop = scrollTop;
      document.documentElement.scrollTop = scrollTop;
      elem.currHeight = parseInt(style.height);

      callback(parseInt(style.height));
    };
  };

  addEvent('propertychange', change);
  addEvent('input', change);
  addEvent('focus', change);
  change();
};

export const debounce = function (func, delay) {
  let timer;

  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay || 500);
  }
}

说明:由于下面是列表,需要计算高度,为了避免重新再去获取高度,所以加了个回调方法,把高度回调出去。

vue-search.vue

<template>
  <div class="search-list">
    <div>
        <textarea ref="textarea" v-model="keywords" :maxlength="keywordsMax" @change="searchChange"></textarea>
        <span class="clear" @click="clearKeywords">x</span>
    </div>
    <div class="list" ref="list">
      <ul>
        <li v-for="item in list" :key="item.id">
          <span class="icon"></span>
          <dl>
            <dt>{{item.title}}</dt>
            <dd>{{item.desc}}</dd>
          </dl>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
  import { debounce, autoTextarea } from '@/util.js';

  let rootFontSize = parseFloat(document.documentElement.style.fontSize);

  export default {
    data () {
      return {
        keywordsMax: 128,
        keywords: '',
        list: []
      }
    },
    mounted () {
        this.$nextTick(() => {
          let textarea = this.$refs.textarea;
          textarea.focus();
          let prevHeight = 65;
          textarea && autoTextarea(textarea, 5, 0, (height) => {
            height += 20;
            if (height !== prevHeight) {
              prevHeight = height;
              let rem = height / rootFontSize;
              this.$refs.list.style.height = `calc(100% - ${rem}rem)`;
            }
          });
        })
    },
    methods: {
      clearKeywords () {
        this.keywords = '';
        this.list = [];
        let textarea = this.$refs.textarea;
        let height = 40;
        let rem = height / rootFontSize;
        textarea.style.height = `${rem}rem`;
        rem = (height + 20) / rootFontSize;
        this.$refs.list.style.height = `calc(100% - ${rem}rem)`;
        textarea.focus();
      },
      searchChange: debounce(function () {
        let trim = this.keywords.trim();
        if (!trim) {
          this.list = [];
          return;
        }
        const params = {
          keywords: this.keywords
        }
        // 调api ...
      })
    }
  }
</script>

补充div模拟textarea自适应

<style>
    .textarea{
        width: 400px;
        min-height: 20px;
        max-height: 300px;
        _height: 120px;
        margin-left: auto;
        margin-right: auto;
        padding: 3px;
        outline: 0;
        border: 1px solid #a0b3d6;
        font-size: 12px;
        line-height: 24px;
        padding: 2px;
        word-wrap: break-word;
        overflow-x: hidden;
        overflow-y: auto;
     
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
    }
</style>

<div class="textarea" contenteditable="true"></div>
posted @ 2019-04-25 18:26  ESnail  阅读(8055)  评论(0编辑  收藏  举报