分类
Javascript

自定义富文本的v-model

为什么要自定义富文本?因为常用的文本域不支持自适应文本高度,看了几个解决方案,界面会闪烁,用户体验不好。最为关键的是,文本域不能插入html标签不支持html渲染。能满足这两个需求的开源编辑器倒是不少,像ueditor、CKeditor,但一般比较重,有些杀鸡焉用牛刀的感觉。下面就用vue来写一个轻量级的富文本组件,并让它支持v-model双向数据绑定。

<template>
  <div
    v-html="html"
    contenteditable="true"
    spellcheck="false"
    class="rich-text"
  >
  </div>
</template>

<script>
export default {
  name: 'RichText',
  data() {
    return {
      html: '<Strong>Hello</Strong>'
    }
  }
}
</script>

<style scoped>
.rich-text {
  white-space: pre-wrap;
  min-height: 48px;
  padding: 4px 8px;
  box-sizing: border-box;
  outline: none;
  word-break: break-all;
  vertical-align: middle;
  border: 1px solid #ddd;
  border-radius: 0;
  user-select: auto;
}
.rich-text:empty::before{
  content: attr(placeholder);
  color: #CCC;
}
</style>

通过设置contenteditable=”true”,指定div元素是可编辑的,使用v-html指令,也说明该元素支持html的。使用CSS伪元素为其添加了占位,比js实现简洁不少。So far so good!为了复用代码,我们接下面将其封装成组件。

要自定义输入框、文本域、下拉框组件的v-model,通常的做法是组件内部定义value属性,派发input事件。做下面的修改:

<template>
  <div
    v-html="value"
    contenteditable="true"
    spellcheck="false"
    class="rich-text"
    @keyup="handleKeyup"
  >
  </div>
</template>

<script>
export default {
  name: 'RichText',
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    handleKeyup(event) {
      this.$emit('input', event.target.innerHTML)
    }
  }
}
</script>

这是调用的代码

<template>
  <div id="app">
    <rich-text v-model="text"/>
    <pre>{{ text }}</pre>
  </div>
</template>

<script>
import RichText from "./components/RichText";
export default {
  name: 'App',
  components: { RichText },
  data() {
    return {
      text: ''
    }
  }
}
</script>

测试一下,咦,怎么光标位置不对,都是在文本开头插入。那是因为处理按键事件有一定延迟,UI重绘之前光标不知道应该定位到何处,所以出了问题。可以在可编辑的元素取得和失去焦点时加个开关处理一下。作如下修改

<template>
  <div
    v-html="html"
    contenteditable="true"
    spellcheck="false"
    placeholder="请输入文本"
    class="rich-text"
    @focus="locked = true"
    @blur="locked = false"
    @keyup="handleKeyup"
  >
  </div>
</template>

<script>
export default {
  name: 'RichText',
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      html: this.value,
      locked: false
    }
  },
  watch: {
    value(newValue) {
      if (!this.locked) {
        this.html = newValue
      }
    }
  },
  methods: {
    handleKeyup(event) {
      this.$emit('input', event.target.innerHTML)
    }
  }
}
</script>

测试一下,搞定!这只是万里长征的第一步,后面还有很多需求。