背景
项目采用vue+Ant Design of Vue,需要在div属性contenteditable为true的实现自定义输入; 具体需求:
- 键盘只能输入0~9,/*-+.
- 通过不同按钮可以插入不同设备名
- 点击不同地方,根据光标位置删除或添加数据
- 最后将输入字符串的中的设备名替换成对应编码值
效果图如下
实现过程
思路
实现需求的关键点
- **如何获取数据?如何便捷编辑数据?**从键盘和按钮点击事件监控每一次输入,并将改变的值存储在数组中,方便增删改查.
- **如何获取光标位置,以及改变光标位置?**根据Selection对象获取光标位置,以及通过collapse方法改变光标位置.
- **如何根据光标位置修改对应数组数据?**新建一个数组储存光标位置与实际数组数据之间的映射关系.例如右侧的按钮组,一个按钮代表一个值.例如点击电量1,在输入框内输入三个字符'电量1'(光标会记录3个位置),但在数据数组上直接储存一个值(数组长度加1),这时候光标位置和对应的数组值的下标会产生偏移.所以需要记录两者的映射关系,用于修改对应数据.
具体代码
<template>
<div>
<a-card>
<a-row>
<a-col :span="16">
<div class="btn-box">
<div>
<a-button
type="primary"
v-for="item in btnList"
:key="item"
style="margin-right: 10px"
@click="setDataList(item, 0)"
>
{{ item }}
</a-button>
</div>
<div>
<a-button @click="onExport" style="margin-right: 10px"> 导出表达式 </a-button>
<a-button style="margin-right: 10px"> 清空 </a-button>
</div>
</div>
</a-col>
<a-col :span="8">
<a-input-search placeholder="input search text" enter-button="查询" size="large" @search="onSearch" />
</a-col>
</a-row>
<a-row>
<a-col :span="16" style="padding: 10px">
<div
ref="editor"
class="box"
contenteditable="true"
@blur="onblur"
@click="onclick"
@focus="onfocus"
@input="oninput"
></div>
</a-col>
<a-col :span="8">
<a-button
v-for="item in textList"
:key="item"
style="margin-right: 10px; margin-top: 10px"
@click="setDataList(item, 1)"
>{{ item }}</a-button
>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script>
const btnList = ['+', '-', '*', '/', '(', ')', 'DEL']
const textList = ['电量1', '电量2', '电量3']
const correspondingTable = {
'+': '+',
'-': '-',
'*': '*',
'/': '/',
'(': '(',
')': ')',
'.': '.',
电量1: '#elect1#',
电量2: '#elect2#',
电量3: '#elect3#',
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 7,
8: 8,
9: 9,
}
export default {
data() {
return {
btnList,
textList,
correspondingTable,
// 真实数据位置
realDomKeys: [],
// 储存对应的字符串值
dataList: [],
// 光标位置
focusOffset: 0,
// 定义最后光标对象
lastEditRange: null,
}
},
mounted() {},
methods: {
setDataList(val, type) {
// 记录数据存储位置(指的是数据所在数组位置的下标值)
let cursorJS
// 记录光标存在位置(指的是当前光标前的数据个数)
let cursorDom = this.focusOffset
// 判断光标所处位置,如果在中文体内,则放到中文右括号侧,其余情况确认光标的真实指向
// 光标是否位于最末尾
if (cursorDom < this.realDomKeys.length && cursorDom > 0) {
// 判断光标是否在中文体内,如果在就让光标落到此中文最右边(右括号侧
if (this.realDomKeys[cursorDom] instanceof Object) {
// 在中文体内
// 记录光标应该在的位置和对应此刻数据存储位置
if (cursorDom == this.realDomKeys[cursorDom].start) {
// console.log('@@光标在中文体旁边')
} else {
cursorDom = this.realDomKeys[cursorDom].start + this.realDomKeys[cursorDom].n
// console.log('@@光标在中文体内')
}
cursorJS = this.getCursorJS(cursorDom) - 1 //取的是当前指向数据的下标值
} else {
// 不在中文体内
cursorJS = this.getCursorJS(cursorDom) - 1
// console.log('@@光标在在中文体外,且在数组内,真实数据位置为', cursorJS)
}
} else if (cursorDom == this.realDomKeys.length) {
// 位于最末尾
cursorJS = this.dataList.length - 1
} else if (cursorDom == 0) {
// 位于最前端
cursorJS = -1
}
// 增减datalist数据
if (val instanceof Object) {
// this.dataList.push(val.name)
} else if (val == 'DEL') {
// 删除数据
let str
if (cursorJS != -1) {
//如果在最前端刪除無效,即cursorJS=-1和0
str = this.dataList[cursorJS]
this.dataList.splice(cursorJS, 1)
this.setRealDomKeys()
if (/.*[\u4e00-\u9fa5]+.*$/.test(str) || str.length > 1) {
cursorDom = cursorDom - str.length - 2
} else {
cursorDom--
}
}
} else {
//添加数据
this.dataList.splice(cursorJS == -1 ? 0 : cursorJS + 1, 0, val)
this.setRealDomKeys()
if (type) {
cursorDom += val.length + 2
} else {
cursorDom++
}
}
this.focusOffset = cursorDom
this.keepLastIndex(this.$refs.editor)
},
// 重新计算dataList的对应realDomKeys
setRealDomKeys() {
this.realDomKeys = []
this.dataList.forEach((item, index) => {
//判断是否为设备名
if (/.*[\u4e00-\u9fa5]+.*$/.test(item) || item.length > 1) {
//凡是包含中文,以及字符串长度大于1的都默认为设备名
let len = item.length + 2
let i = 0
let reaLen = this.realDomKeys.length
while (i < len) {
this.realDomKeys.push({
index: index, //对应数据数组的下标值
start: reaLen, //此数据在realDomKeys起始下标
n: len, //共占有多少数据格
})
i++
}
} else {
this.realDomKeys.push(index)
}
})
},
// 获取当光标不在中文体内时,对应的数据位置
getCursorJS(cursorDom) {
let count = 0
let i = 0
while (i < cursorDom) {
if (this.realDomKeys[i] instanceof Object) {
count++
i += this.realDomKeys[i].n
} else {
count++
i++
}
}
return count
},
// 检查输入是否是数组或者+-/*.0-9
oninput(e) {
var text
if (!e.data) {
this.setDataList('DEL', 0)
} else {
text = e.data.replace(/[^\d\/\*\-\+\.]/g, '')
}
if (text) {
this.setDataList(text, 0)
}else{
this.keepLastIndex(this.$refs.editor)
}
},
keepLastIndex(obj) {
if (window.getSelection) {
obj.focus()
// 获取选定对象
var selection = getSelection()
if (this.lastEditRange) {
// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
selection.removeAllRanges()
selection.addRange(this.lastEditRange)
}
obj.innerHTML = this.getHtml()
selection.collapse(selection.anchorNode.childNodes[0], this.focusOffset)
}
},
// 将存储数据转化成html
getHtml() {
let str = ''
this.dataList.forEach((item) => {
if (/.*[\u4e00-\u9fa5]+.*$/.test(item) || item.length > 1) {
str += `(${item})`
} else {
str += item
}
})
return str
},
// 點擊editor時獲取光標位置
onclick(e) {
let selection = window.getSelection()
this.lastEditRange = selection.getRangeAt(0)
this.focusOffset = selection.focusOffset
},
// 将存储数据转化成後端字符串形式
onExport() {
let str = ''
this.dataList.forEach((item) => {
if (/.*[\u4e00-\u9fa5]+.*$/.test(item) || item.length > 1) {
str += this.correspondingTable[item]
} else {
str += item
}
})
console.log(str,JSON.stringify(this.dataList))
},
onblur() {
console.log('onblur')
},
onfocus() {
// console.log('onfocus', window.getSelection(), document.getElementById('editor'))
},
onSearch(value) {
console.log(value)
},
},
}
</script>
<style scoped>
.box {
border-radius: 5px;
border: 2px solid #000;
height: 300px;
padding: 5px;
}
.btn-box {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.text {
font-weight: 600;
color: aquamarine;
}
</style>
重点分析
设置光标位置核心代码.将div内文本字符串看作一个选区,传入collapse方法中,通过设置落在节点的偏移量来改变光标位置
keepLastIndex(obj) {
if (window.getSelection) {
obj.focus()
// 获取选定对象
var selection = getSelection()
if (this.lastEditRange) {
// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
selection.removeAllRanges()
selection.addRange(this.lastEditRange)
}
obj.innerHTML = this.getHtml()
selection.collapse(selection.anchorNode.childNodes[0], this.focusOffset)
}
},
通过监测input( @input="oninput")输入,用正则判断输入的值是否为理想值,如果是则将值存储到数组中,反之不存储.
// 检查输入是否是数组或者+-/*.0-9
oninput(e) {
var text
if (!e.data) {
this.setDataList('DEL', 0)
} else {
text = e.data.replace(/[^\d\/\*\-\+\.]/g, '')
}
if (text) {
this.setDataList(text, 0)
}else{
this.keepLastIndex(this.$refs.editor)
}
},