【Vue组件】从零开始实现一个支持插入自定义表情的评论组件

  近期由于产品迭代,需要新增一个评论功能,且需要支持插入自定义表情。评论功能很多人一开始跟我一样,第一个想到的就是用textarea,但是textarea是不支持的插入图片的,因为我们的表情包是以图片的形式插入文本中的,所以这里是使用HTML5的新特性contenteditable,让div里的内容变成可编辑的。
demo地址

先看下效果图(目前已支持多套表情,详情请看demo):

在这里插入图片描述

组件功能:

  • 支持插入自定义表情
  • 字符验证,超出部分自动切割(以字符进行计算,不是以长度进行计算,一个中文2个字符,一个字母1个字符,一个表情4个字符),因为我们公司业务是使用字符进行字数统计,所以验证是用字符,需要用length可以自行修改
  • 在光标位置准确插入表情
  • 支持多套表情
  • 支持复制内容标签过滤

遇到的问题:(先列出问题,后面再写解决方法)

  • 如果有对显示的内容进行强制更新时(如插入表情包,或者字符切割),光标会跑到文本最前面
  • 无法直接利用vue进行双向绑定,无法用v-model与v-html进行数据双向绑定
  • 定位光标位置,如果是在输入时就点击表情包 应该插入到最光标位置,如果不是在输入时候点击表情应该插入到文本到最后面
  • 进行文本字节计算时对表情的处理,表情占位符如何保证唯一
  • 超出字数限制如何切割 当超出的字符是表情的时候怎么切割
  • 按下回车会插入<div><br></div>标签,或者会用<div>标签包裹住文本
  • 按下空格符会有&nbsp;,以字符进行计算的话空格符&nbsp;会被当成6个字符,实际上空格代表1个
兼容性问题:
  • IE9以及部分Safari createContextualFragment 不兼容该方法
  • window.getSelection , window.getSelection().getRangeAt这两个方法的兼容性,由于我们的业务不需要兼容低版本的IE所以这里我就没做兼容

开始实现

html部分于css部分比较简单就不过多赘述,先不贴代码了,文末看完整代码或者直接下载完整小demo运行看看

参数

既然是封装成组件,那就需要从父组件那里接收一下数据

  submitCmtLoading: {
      //  是否正在提交,如果是正在提交就不需要重复提交了
      type: Boolean,
      default: false
    },
    limtText: {
       //  限制的字符数,默认是0,0是不需要限制
      type: Number,
      default: 0
    },
    iconWidth: {
     //  插入的表情宽度
      type: Number,
      dafault: 24
    },
    iconHeight: {
     //  插入的表情高度
      type: Number,
      dafault: 24
    },
    cmt_show: {
     // 是否显示组件
      type: Boolean,
      default: true
    },
    iconList: {
     // 表情包列表,以数组的形式且是必传项
      type: Array,
      required: true
    },
    placeholder: {
     // 提示语
      type: String,
      default: "积极回复可吸引更多人评论"
    },
    info: {
     // 信息,这个看业务需求,如果需要对具体项在子组件中进行处理可以传递这个
      type: Object
    }

由于还没贴上html代码,下文中出现的this.$refs.cmt_input都代表输入框,既带属性contenteditable的元素

数据
  data: function() {
   
    return {
   
      content: "",  //  评论输入的内容
      widFouce: "",   // 用于定位输入框失焦点前的光标位置
      rangeFouce: "",  // 用于定位输入框失焦点前的光标位置
      iconShow: false,  // 是否显示表情列表
      isSubmit: false  //  能否提交  如果输入内容为空是不给提交的
    };
  },
  watch: {
   
    cmt_show: {
      //  当组件显示时需要将光标定位到文末,不然第一次点击表情包会报错
      handler(value) {
   
        if (value) {
   
          this.$nextTick(() => {
   
            this.toLast(this.$refs.cmt_input);  //  将光标定位到文末
          });
        }
      },
      immediate: true
    }
  },

在表情列表展开的时候,点击其他非表情列表区域要让这个表情列表消失。

 mounted() {
   
    const self = this;
    document
      .getElementsByClassName("g-doc")[0]
      .addEventListener("click", self.setHideClick);     //  g-doc是根节点,当然也可以换成body啥的,但是在组件销毁的时候要把时间也给移除掉
    self.$once("hook:beforeDestroy", () => {
   
      document.getElementsByClassName("g-doc")[0] &&
        document
          .getElementsByClassName("g-doc")[0]
          .removeEventListener("click", self.setHideClick);
    });
  },
  //  setHideClick 方法,放在method里的
      setHideClick(event) {
   
      let target = event.srcElement;
      let nameList = ["icon_item", "icon_list", "icon_box"];  // 这个三个类名是整个表情包列表
      if (
        !nameList.includes(target.className) &&
        !nameList.includes(target.parentElement.className)
      ) {
    // 如果不是表情包列表区域就关闭表情包列表面板
        this.iconShow = false;
      }
    },
方法

几个简单点的方法,不过多的讲解

问题:如果有对显示的内容进行强制更新时(如插入表情包,或者字符切割),光标会跑到文本最前面
解决方法: 每次进行更新操作后调一次toLast(obj)方法,让光标回到最后面, 当然了,如果光标不在最后面的就不需要调这个方法了

    toLast(obj) {
   
      // 将光标移到最后,obj为输入框节点
      if (window.getSelection) {
   
        let winSel = window.getSelection(); 
        winSel.selectAllChildren(obj);
        winSel.collapseToEnd();
      }
    },
    blurInput() {
    // 失焦时触发的方法,失焦的时候保存光标位置
      this.getFouceInput();
    },
    getFouceInput() {
    // 保存光标位置
      this.widFouce = window.getSelection();
      this.rangeFouce = this.widFouce.getRangeAt(0);
    },
    showIconListPlane() {
    // 显示或者关闭表情包列表
      // 显示表情包列表
      if (!this.iconShow) {
    //  如果是打开,要保存一下光标位置
        this.getFouceInput();
      }
      this.iconShow = !this.iconShow;
    },
     submitCmt() {
   
      // 提交评论
      const self = this;
      let text = this.$refs.cmt_input.innerHTML;
      let length = this.getCharLen(this.paseText(text).text);
      if (self.submitCmtLoading || !self.isSubmit) return;
      if (self.cmt_text == "") {
   
        self.$emit("submitError", "评论为空");  // 不符合规则时则抛出错误的方法submitError
        return;
      } else if (this.limtText && length > this.limtText) {
   
        self.$emit("submitError", "评论超出字数"); // 不符合规则时则抛出错误的方法submitError
        return;
      }
      self.$emit("submitSuccess", text, self.info); // 验证通过则抛出正确的方法,且将文本与信息同时传递给父组件
    }
字符验证

接下来讲一下字符验证的函数,因为光标相关的逻辑里有用到部分字符验证,所以先讲一下字符相关的方法。
判断文本占了多少个字符,这里传进来的文本是经过处理的,因为如果没处理过的文本里如果带表情包,而表情包是以img标签存在文本里的这样会占据很多个字符,所以会对表情包做一个占位符处理,让一个表情包只占4个字符,后面会讲这个。现在先看下判断字符个数的方法,因为如果是输入空格会变成&nbsp;所以在这个方法里也会对这个进行处理

    getCharLen(sSource) {
   
      // 空格&nbsp; 要当1个字符算,所以最后要给每个空格减去5
      // 获取字符长度
      var l = 0;
      var schar;
      for (var i = 0; (schar = sSource.charAt(i)); i++) {
   
        l += schar.match(/[^\x00-\xff]/) != null ? 2 : 1;
      }
      let nbsp = sSource.match(/&nbsp;/gi);
      if (nbsp) {
   
        let len = nbsp.length;
        l = l - len * 5;
      }

      return l;
    },

使用占位符来替代表情包,因为1个表情包为4个字节,所以用4个数字来占位。这里使用随机获取时间戳的4为数,然后与文本进行比较,如果文本中存在,则递归再次获取,直到文本中不存在这4个数字。
对于回车键会让文本中添加div与br标签,因为我们的评论是不允许手动换行的,所以把div标签与br标签都替换成两个空格“ ”注意不是&nbsp;

这也就解决了上面说的这三个问题

  • 进行文本字节计算时对表情的处理,表情占位符如何保证唯一
  • 按下回车会插入<div><br></div>标签,或者会用<div>标签包裹住文本
  • 按下空格符会有&nbsp;,以字符进行计算的话空格符&nbsp;会被当成6个字符,实际上空格代表1个
    getRandomFour(str) {
   
      // 在时间戳里取随机4位数作为key,且如果文本中包含key则递归
      let timeStr = new Date().getTime() + "";
      let result = "";
      for (let i = 0; i < 4; i
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值