接到一个需求,要求实现一个类似富文本输入框,输入$之后弹出候选列表进行选择,点击后选项输入到输入框内,展示为类似一个标签,退格时删除整个标签,保存时校验公式合法性。

Java前端富文本框中文乱码 前端实现富文本编辑器_富文本

 

  一、实现思路:

 

  1. 富文本框实现

contentEditable设置为true可将任意标签设置为可编辑标签,在标签内输入任意html文档。

  文本编辑器:当然也有现成的富文本编辑器本身就在带@呼出列表的功能,但是存在两个问题:一是富文本编辑器太重,我们的需求只需要到富文本编辑器中的很少一部分功能;二是富文本编辑器存在一个问题(至少wangedit),编辑器内部是通过伪元素的方式实现退格删除整个标签,但是没有解决退格删除标签时导致光标输入光标定位不准确问题,在网上查阅了很多实现方案后,在Stack Overflow上找到另外一种方式实现标签的效果,那就是使用input标签来替代伪元素。将input标签的type设置为button,再修改样式去掉背景边框等,即可实现标签效果,退格删除也会删除整个input元素。

  2. 校验四则运算的合法性

 

  最初是希望通过正则表达式来校验公式的合法性,但是由于规则过于复杂,最终选了使用js的eval函数,利用eval函数解析和执行代码的功能,将式子传入进去,如果执行成功,则表示式子合法,如果报语法错误,则意味着公式不合法。

  二、代码片段

<div
      ref="editor"
      :contentEditable="true"
      @keydown="enterEv($event)"
      @click="onClickEditor"
      @input="inputChange($event)"
      @οnpaste="() => false"/>

 

监听键盘事:

// keydown事件
    enterEv(e) {
      if (e.key === '$') { // 如果是$,阻止输入,弹出选择弹窗
        e.preventDefault();
        this.setRecordCoordinates(); // 保存当前光标的坐标,选择之后要插入到当前位置 
        this.showSelectPop(); // 展示选择弹窗
      } else {
        const mathReg = /^[0-9.+\-*/() ]|(Backspace)|(ArrowLeft)|(ArrowRight)|(ArrowDown)|(ArrowUp)|(Shift)+$/;
        if (!mathReg.test(e.key)) {
          e.preventDefault();
        }
      }
    },

获取当前光标位置:

// 获取当前光标坐标
    setRecordCoordinates() {
      try {
        // getSelection() 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。
        const selection = window.getSelection();
        this.lightPosition = {
          range: selection.getRangeAt(0), // 返回range对象
          selection: selection,
        };
      } catch (error) {
        console.log(error, '获取光标位置失败');
      }
    },

选择插入的项之后,生成对应的节点并插入到保存的光标位置:

(这里需要注意,在进行点击选择某一项的时候,当前光标会自动定位到该位置,所以我们需要取出上一步保存的位置,将光标恢复到该位置,然后插入生成的input节点)

createSelectElement({name, uuid}) {
      const {range, selection} = this.lightPosition;
      // 生成需要显示的内容
      const inputNode = document.createElement('input');
      inputNode.value = `\${${name}}`; // $的文本信息
      inputNode.type = 'button';
      inputNode.dataset.id = uuid; // 用户ID、为后续解析富文本提供
      // spanNodeFirst.contentEditable = false // 当设置为false时,富文本会把成功文本视为一个节点。
      inputNode.className = 'tag';

      const frag = document.createDocumentFragment();
      frag.appendChild(inputNode);
      if (range) {
        range.insertNode(frag);
      }
      this.showPop = false;
      /*
      1. 点击选择插入内容时selection会重新设置range,为了重新将光标定位到插入内容后方,需要清除range,启用已保存的range,
      2. 由于原先range为未选状态,设置rang的位置被插入的内容所替换,所以重置range后插入内容会处于选择状态,调用setRecordCoordinates取消当前选区,并把光标定位在原选区的最末尾处。
      3. 更新存储的光标信息
       */
      this.$nextTick(() => {
        selection.removeAllRanges(); // 移除当前光标
        selection.addRange(range); // 还原光标位置
        selection.collapseToEnd(); // addRange之后光标处于选中状态,需要将光标移动至最末端
        this.setRecordCoordinates(); // 更新存储的光标信息
        this.inputChange();
      });
    },

每次点击事件也要更新光标位置:

// 点击事件的触发函数,每次点击获取更新坐标
    onClickEditor() {
      this.setRecordCoordinates();
    },

弹窗代码:

弹窗实现比较简单,只需要在选择项之后调用createSelectElement函数生成节点并关闭自身即可。

 

 公式校验:

  把最终得到的式子中的变量替换为数字1,然后传入eval函数

// 校验公式合法性,利用eval函数能够执行计算式子的特性去校验
      const REG = /(\$\{.+?\})/g;
      const strList = str.split(REG);
      const newList = strList.map(str => {
        return REG.test(str) ? 'a' : str;
      });
      try {
        const res = eval('let a = 1;' + newList.join(''));
        if (res === Infinity) {
          this.$hMessage.warning('请检查计算公式是否正确');
          return false;
        }
      } catch (e) {
        this.$hMessage.warning('请检查计算公式是否正确');
        return false;
      }