likes
comments
collection
share

「CSS」实现可编辑的文本输入框

作者站长头像
站长
· 阅读数 11

前言

我们是用 textarea 高度随着内容的改变而自动改变,还是用 div 模拟 textarea 呢? 两者都能实现,实现原理似乎不太一样,选用哪一个,就要看产品的需求和哪一个需要填的坑最少了。在一次开发中兜兜转转,我最后选择了用 textarea ,原因和之前的项目保持一致的风格。还是那句老话,如果在项目中有很多地方需要使用,可以考虑使用一个公共的组件库,后期代码维护方便。如果是一个团队的多个项目中使用,可以写成一个公共的 UI 库,通过 npm 安装使用。

需求

根据输入内容自动撑开元素的高度,避免滚动条里面嵌套滚动条的尴尬局面。如果文本输入框元素后面有元素会被自动挤下去,整体的体验应该算是挺不错的,所有内容通过一个滚动就可以展示完。

textarea

一个文本输入框,一个可以无限输入内容的标签,文字超出高度,会自动有一个滚动条。在工作中用的比较少,第一时间想到 textarea ,可是在网上都是看到通过 cols 去控制行高,还有不太美观的滚动条,高度要实时控制,需要去监听输入内容的变化,一变化就改变 cols 的大小,我无法知道用户输入的是换行还是字符串,什么时候进行 cols 的大小变化。

div 模拟 textarea

  • 后来想到用 div 模拟 textarea,设置 contenteditable="true"
  • 那怎么模拟 placeholder 呢? 查询资料找到了解决方案。
.textarea:empty:before {
  content: attr(placeholder);
  color: rgba(144, 144, 144, 1);
  line-height: 24px;
}
.textarea:focus:before {
  content: none;
}
  • IOS 底下不能编辑,解决方案 -webkit-user-select: text;
  • 安卓或者 IOS 在聚焦的时候第一次总是没法聚焦,要点两次。

解决方案如下代码

// 解决焦点聚焦时候的问题
let textarea = this.$refs.textarea
textarea.focus()
if (typeof window.getSelection !== 'undefined' &&
typeof document.createRange !== 'undefined') {
    var range = document.createRange()
    range.selectNodeContents(textarea)
    range.collapse(false)
    var sel = window.getSelection()
    sel.removeAllRanges()
    sel.addRange(range)
} else if (typeof document.body.createTextRange !== 'undefined') {
    var textRange = document.body.createTextRange()
    textRange.moveToElementText(textarea)
    textRange.collapse(false)
    textRange.select()
}

这一次我以为完美实现了。让我崩溃的事再一次发生了,模拟的输入框每一次换行都会被一个 div 标签包着,而且我获取到的内容是带有标签的(span, br),这个导致的直接影响就是,我并不知道哪些人会用,麻烦太大了。后来我又发现可以通过使用 -webkit-user-modify: read-write-plaintext-only; 进行控制,让只能输入纯文本,问题是解决了。

关于手机的兼容性,只是简单的对比了 IOS 和安卓两个系统,还有更多机型没有仔细测试。

回过头再来看看 textarea

因为div 模拟 textarea,不知道有多少坑等着我去踩,对桌说了一句是标签就有 height 这些属性。在我的潜意识里面居然忽略了这个,一语惊醒梦中人。

继续研究用 textarea,实现,发现了利用 Element.scrollHeight 这个只读属性可以实现(一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。)

<textarea maxlength='1000' ref="textarea" class='textarea' v-model='sendContent' placeholder="请输入文本内容" @input="resizeHeight">

// 函数节流,不实时改变数据
 function throttle (fn, interval = 300) {
  let canRun = true
  return function () {
    if (!canRun) return
    canRun = false
    setTimeout(() => {
      fn.apply(this, arguments)
      canRun = true
    }, interval)
  }
},
created(){
  //函数节流,需要先执行一次,形成必包。如果在methods里面,每一次调用都是生成新的函数,没有节流的效果
  this.resizeHeight = this.throttle(this.resize, 1000)()
}

resize () {
  let text = this.$refs.textarea
  text.style.height = 'auto'
  text.style.height = text.scrollHeight + 'px'
},

//去掉默认样式
  overflow-y: hidden;
  outline: none;
  resize: none;
  border: none;

注意了:当我们设置了 textareadisplay 时候,在 IOS 下面字体颜色灰蒙蒙的,尝试过给 textarea: dispay 添加属性去修改字体的颜色,但是好像都失效了 。最后改用了 readonly 去实现,需要加下面一段代码,否则会有光标在那显示

[readonly="readonly"] {
  user-select: none;
}

white-space

或许你都没有听说过这个属性,但是在关键时刻很好用,是他帮助我解决了一个很大的难题。前面说到过 div 模拟 textarea。如果是用 div 单纯的去显示 textarea 内容,在 vue 中可以用 v-html 去渲染,但是如果接口返回的数据把换行转成是一个空格,这时候如果我们没有进行处理的话,换行会失效。可以选择用 js 进行处理,但是没必要,因为 css 可以用 white-space: pre-wrap; 解决,就是这么简单。

总结

或许还有更多更好的实现方式,多个脑袋总会比一个脑袋好用,欢迎大家和我交流。兜兜转转最后我还是选择了使用 textarea ,转了一个那么大的圈,回到最初,只想说,革命尚未成功,同志仍需努力。

这些文章写的都挺不错的