接到一个需求,要求实现一个类似富文本输入框,输入$之后弹出候选列表进行选择,点击后选项输入到输入框内,展示为类似一个标签,退格时删除整个标签,保存时校验公式合法性。
一、实现思路:
- 富文本框实现
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;
}