div contenteditable属性下限制性输入以及自定义光标位置
背景
项目采用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> <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" 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) } },
作者:流码
链接:https://juejin.cn/post/7038884730506313741