前言: 最近系统中需要增加一个自定义公式功能,大致的功能就是让用户能够根据自己的需求,去定义表格中的某个列根据自定义的公式得出的结果,效果图如下![在这里插入图片描述](https://img-blog.csdnimg.cn/20210629114417731.gif)
上面的git图可以分析出需求
- 支持直接键盘手动输入,#号呼出变量选择,并且变量支持输入匹配
- 支持点击虚拟键盘上数字与运算符,变量等进行输入
- 数字,运算符,变量等不同类型的字符在下面的框中要展示不同的样式
开始根据需求分析实现方案
- 项目使用的ant的UI框架,文档中查到Mentions组件符合第一条需求,故而选择
- 虚拟键盘点击事件与渲染都没什么问题,关键点如何在用户移动光标不在字符最后的时候,如何进行光标插值,这里通过document.selection.createRange实现
- 不同类型字符展示不同样式,实现逻辑就是不断正则匹配,然后字符串切割
代码实现
1, Mentions基本都满足了第一需求,这里我配置的呼出的关键词是#,选择变量后的尾缀增加一个空格字符,这个很重要,后续跟匹配变量的正则需要对应上
<a-mentions class="input-box" @change="changeHtml" ref="varBox" v-model="currentVar" :prefix="'#'" autoFocus >
<a-mentions-option v-for="value in varArr" :key="value.value" :value="value.value">
{{ value.label }}
</a-mentions-option>
</a-mentions>
2, 在光标处插值,需要利用range
cursorPostion(){
let _this = this
this.cursorPositionObj = {
get: function (textarea) {
// 获取光标位置,传入的参数需要是一个input或者textarea的dom元素
var rangeData = { text: "", start: 0, end: 0 };
if (textarea.setSelectionRange) { // W3C
textarea.focus();
rangeData.start = textarea.selectionStart;
rangeData.end = textarea.selectionEnd;
rangeData.text = (rangeData.start != rangeData.end) ? textarea.value.substring(rangeData.start, rangeData.end) : "";
} else if (document.selection) { // IE
textarea.focus();
var i,
oS = document.selection.createRange(),
// Don't: oR = textarea.createTextRange()
oR = document.body.createTextRange();
oR.moveToElementText(textarea);
rangeData.text = oS.text;
rangeData.bookmark = oS.getBookmark();
// object.moveStart(sUnit [, iCount])
// Return Value: Integer that returns the number of units moved.
for (i = 0; oR.compareEndPoints('StartToStart', oS) < 0 && oS.moveStart("character", -1) !== 0; i++) {
// Why? You can alert(textarea.value.length)
if (textarea.value.charAt(i) == '\r') {
i++;
}
}
rangeData.start = i;
rangeData.end = rangeData.text.length + rangeData.start;
}
return rangeData;
},
set: function (textarea, rangeData) {
var oR, start, end;
if (!rangeData) {
alert("You must get cursor position first.")
}
textarea.focus();
if (textarea.setSelectionRange) { // W3C
textarea.setSelectionRange(rangeData.start, rangeData.end);
} else if (textarea.createTextRange) { // IE
oR = textarea.createTextRange();
// Fixbug : ues moveToBookmark()
// In IE, if cursor position at the end of textarea, the set function don't work
if (textarea.value.length === rangeData.start) {
//alert('hello')
oR.collapse(false);
oR.select();
} else {
oR.moveToBookmark(rangeData.bookmark);
oR.select();
}
}
},
add: function (textarea, rangeData, text) {
/*
* 1, textarea -- textarea原生dom
* 2, rangeData -- 当前光标处,可以通过上面的get方法获取返回值之后,当参数传入即可
* 3, text -- 需要追加的文本内容
* */
var oValue, nValue, oR, sR, nStart, nEnd, st;
this.set(textarea, rangeData);
if (textarea.setSelectionRange) { // W3C
oValue = textarea.value;
nValue = oValue.substring(0, rangeData.start) + text + oValue.substring(rangeData.end);
nStart = nEnd = rangeData.start + text.length;
st = textarea.scrollTop;
textarea.value = nValue;
_this.currentVar = nValue;
// Fixbug:
// After textarea.values = nValue, scrollTop value to 0
if (textarea.scrollTop != st) {
textarea.scrollTop = st;
}
textarea.setSelectionRange(nStart, nEnd);
} else if (textarea.createTextRange) { // IE
console.log("IE");
sR = document.selection.createRange();
sR.text = text;
sR.setEndPoint('StartToEnd', sR);
sR.select();
}
}
}
},
内容追加在用户点击的时候,将对应的内容追加进去即可
setInputValue(value){
// 从输入框的光标处,插入字符
let cur = this.cursorPositionObj.get(this.textareaDom)
this.cursorPositionObj.add(this.textareaDom,cur,value)
},
当内容改变时,使用正则匹配,转成不同的html内容,以表示不同的样式
1. 定义3种类型的正则,分别是运算符,数字,变量
operationReg:/[\+\-\*//()]/, // 运算符正则
numberReg:/\d+(\.\d+)?/, // 数字正则,不限制小数点
varReg:/^#\S*\s/, // 变量的正则,这里对应上面的变量唤起的关键字的#号开头,空格结尾,如果修改,对应修改即可
2,这里还有一个点,如果当+ -这样出现时,那这时候的-号,是作为负号,也就是当做跟数字当做同类型,所以需要记录变量的顺序
typeOrder:[], // 记录字符类型的顺序
3,定义方法,在每一次输入框内容改变的时候调用,正则方式使用match,可以拿到匹配到的字符,与字符出现的位置
filterStr(str){
/*
* 1, 根据正则匹配字符,根据index的大小进行判断当前的字符的类型是什么
* 2, 然后根据index加上匹配到的字符长度,进行原字符的切割,然后继续匹配,一直匹配到没有字符为止
* 3, 记录字符类型的出现的顺序,因为如果-号前面是运算符的话,那它将不是运算符,而是负数的数字
* */
if(!str){
return;
}
// 将字符头部的空格字符去掉,否则影响变量的匹配
str = str.replace(/^\s+/,'');
let arr = [
{match:'varReg',index:null},
{match:'operationReg',index:null},
{match:'numberReg',index:null},
]
arr.map((item)=>{
item.match = str.match(this[item.match])
if(item.match !== null){
item.index = item.match.index
}
})
// 比较匹配到的正则谁在最前面
let res = arr.reduce(function(prev,cur){
if(cur.index !== null && prev.index !== null){
return cur.index < prev.index ? cur : prev
}else{
return cur.index !== null ? cur : prev
}
})
if(res.index !== null){
// 将匹配到的字符用数组保存起来
this.strArr.push(res.match[0])
// 这里将已经匹配的字符去掉,剩下的字符继续匹配,一直到结束
this.filterStr(str.slice(res.index + res.match[0].length))
}
},
输入框的内容改变时,需要将字符串数组与类型数组置空,然后将文本变成数组,再将数组进行匹配转成HTML
changeHtml(){
this.strArr = [];
this.typeOrder = [];
let strHtml = ''
let txt = this.currentVar; // 这里是输入框内容
// 虚拟键盘中的乘号与除号需要替换成为真正的运算符
txt = txt.replace(/x/g,'*')
txt = txt.replace(/÷/g,'/')
this.filterStr(txt) // 将文本内容转成到数组
// 将字符转换成html进行格式化
this.strArr.map((item,index)=>{
if(this.varReg.test(item)){
this.typeOrder.push('var')
strHtml += `<span class="c-2979FF bg-D8E6FF radius-4">${item}</span>`
}else if(this.operationReg.test(item)){
if(item == '-' && this.typeOrder.pop() == 'operation'){
// 如果当前字符是-,并且上一个字符类型是运算符,则当前的-当做数字处理
strHtml += `<span class="c-65B168">${item}</span>`
}else{
strHtml += `<span class="c-F65F54">${item}</span>`
}
this.typeOrder.push('operation')
}else if(this.numberReg.test(item)){
this.typeOrder.push('number')
strHtml += `<span class="c-65B168">${item}</span>`
}
})
this.statisticsHtml = strHtml;
},