乐趣区

关于javascript:高度动态伸缩的输入框

上周工作中在设计图上看到了这样一个 textarea 框,只有底下一条线,没有高度

也就是说输入框高度不是固定的,是由输出内容决定的

思路

看到这个设计想了想没思路,立即去找度娘,网上支流解决方案有 2 种:

  • 监听 input 事件获取 textarea 的滚动高度,调节款式
  • 属性 contenteditableheight:auto的 div 能够编辑内容,取代textarea

1. 通过事件调节高度

<template>
  <div class="flexable-textarea">
  <div ref="simulateTextarea" :style="{'padding':padding}" class="simulate-textarea">{{value || '0'}}</div>
    <textarea
      ref="editTextarea"
      :value="value"
      :maxlength="maxLength"
      :style="{'padding':padding}"
      :placeholder="placeholder"
      class="edit-textarea"
      @input="handleInput($event)">
    </textarea>
  </div>
</template>

<script>
/* 高度自适应的 textarea */
export default {
  name: 'FlexableTextarea',
  props: {
    padding: {
      type: String,
      default: '20px 0'
    },
    value: {
      type: String,
      default: ''
    },
    maxLength: {
      type: Number,
      default: 999999
    },
    placeholder: {
      type: String,
      default: ''
    }
  },
  watch: {value(val, oldVal) {if (val !== oldVal) {this.check()
      }
    }
  },
  mounted() {this.check()
  },
  methods: {
    // 检测高度
    check() {this.$nextTick(() => {
        const textarea = this.$refs.editTextarea
        const simulate = this.$refs.simulateTextarea
        if (textarea.style.height !== `${simulate.scrollHeight}px`) {textarea.style.height = `${simulate.scrollHeight}px`
        }
      })
    },
    handleInput(event) {if (event.target.value !== this.value) this.check()
      this.$emit('input', event.target.value)
    }
  }
}
</script>
<style lang="scss" scoped>
.flexable-textarea {
  position: relative;
  overflow: hidden;
  font-size: 28px;
  line-height: 48px;
  padding: 0;
  .edit-textarea {
    box-sizing: border-box;
    font-size: inherit;
    line-height: inherit;
    white-space: pre-wrap;
    overflow-wrap: break-word;
    display: block;
    width: 100%;
    height: auto;
    min-height: 48px;
  }
  .simulate-textarea {
    box-sizing: border-box;
    position: absolute;
    left: 0;
    top: 0;
    z-index: -1;
    opacity: 0;
    width: 100%;
    height: auto;
    min-height: 48px;
    font-size: inherit;
    line-height: inherit;
    white-space: pre-wrap;
    overflow-wrap: break-word;
  }
}
</style>

HTML 构造并不简单,但有人会问为什么不间接获取 textarea 的高度,还要做个暗藏的 div 容器把 value 再复制一遍呢?因为 textarea 的个性是能够被撑开,但不会本人膨胀,设置款式 height:auto 在输出很多行后再删除几行,它的高度是不会变的。所以须要借助其余容器拿到 scrollHeight,曲线救国。
长处:
兼容性好
毛病:
1. 设置高度时局部浏览器有卡顿感。
2. 如果组件一开始暗藏再显示,须要手动调用 check 办法,不够洁净。

2. 用 div 代替 textarea

<template>
  <div :style="{padding}" class="flex-input-wrapper" @click.stop="onFocus($event)">
    <div
      ref="flexInput"
      :placeholder="placeholder"
      class="flex-input"
      contenteditable="true"
      @input="changeText($event)"
    ></div>
  </div>
</template>
<script>
/* 高度自适应的 input */
export default {
  name: 'FlexableInput',
  props: {
    padding: {
      type: String,
      default: '20px 0'
    },
    value: {
      type: String,
      default: ''
    },
    maxLength: {
      type: Number,
      default: 999999
    },
    placeholder: {
      type: String,
      default: ''
    }
  },
  watch: {value(newValue) {
      const ele = this.$refs.flexableInput
      const innerText = ele.innerText
      if (newValue !== innerText) {this.setValue(newValue)
      }
    }
  },
  mounted() {this.setValue(this.value)
  },
  methods: {setValue(value = '') {if (value.length === 0) return
      const _val = value.length < this.maxLength ? value : value.substring(0, this.maxLength)
      this.$refs.flexableInput.innerText = _val
    },
    changeText(event) {
      const ele = event.target
      let innerText = ele.innerText
      if (innerText.length > this.maxLength) {innerText = innerText.substring(0, this.maxLength)
        ele.innerText = innerText
        this.keepLastIndex(ele)
      }
      this.$emit('input', innerText)
    },
    onFocus(event) {
      const input = this.$refs.flexableInput
      if (document.activeElement === input) return
      this.keepLastIndex(input)
    },
    // 固定光标到最初
    keepLastIndex(obj) {if (window.getSelection) {
        // ie11 10 9 ff safari
        obj.focus() // 解决 ff 不获取焦点无奈定位问题
        const range = window.getSelection() // 创立 range
        range.selectAllChildren(obj) // range 抉择 obj 下所有子内容
        range.collapseToEnd() // 光标移至最初} else if (document.selection) {
        // ie10 9 8 7 6 5
        const range = document.selection.createRange() // 创立选择对象
        // var range = document.body.createTextRange();
        range.moveToElementText(obj) // range 定位到 obj
        range.collapse(false) // 光标移至最初
        range.select()}
    }
  }
}
</script>

<style lang="scss" scoped>
.flexable-input {
  outline: none;
  user-select: text;
  cursor: text;
  width: 100%;
  font-size: 28px;
  line-height: 48px;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  &:empty::before {content: attr(placeholder);
    color: #999;
  }
}
</style>

代码更加简略了,也没有前一种计划的毛病。惟一的瑕疵是点击不够灵活,div 常常获取不到焦点,因而加上了 click 事件。

退出移动版