div contenteditable属性下限制性输入以及自定义光标位置

6,249 阅读3分钟

背景

项目采用vue+Ant Design of Vue,需要在div属性contenteditable为true的实现自定义输入; 具体需求:

  • 键盘只能输入0~9,/*-+.
  • 通过不同按钮可以插入不同设备名
  • 点击不同地方,根据光标位置删除或添加数据
  • 最后将输入字符串的中的设备名替换成对应编码值

效果图如下 image.png

实现过程

思路

实现需求的关键点

  1. **如何获取数据?如何便捷编辑数据?**从键盘和按钮点击事件监控每一次输入,并将改变的值存储在数组中,方便增删改查.
  2. **如何获取光标位置,以及改变光标位置?**根据Selection对象获取光标位置,以及通过collapse方法改变光标位置.
  3. **如何根据光标位置修改对应数组数据?**新建一个数组储存光标位置与实际数组数据之间的映射关系.例如右侧的按钮组,一个按钮代表一个值.例如点击电量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)
      }
      
    },

参考资料

  1. Selection
  2. html元素contenteditable属性如何定位光标和设置光标