使用contenteditable实现模板输入框

1,257 阅读3分钟

需求

需求是这样的,有一个输入框需要输入特殊的变量,后续使用时动态替换变量类容

这是一个模板输入{变量1},其他输入{变量2}

输入可以用一个变量可以使用下拉框选择,像这样

image.png

但是这样体验并不好,变量可以删除一部分,而且没有高亮

所以想要这样的效果

image.png

实现原理

实现原理就是使用富文本输入框,变量使用contenteditable="false"元素,这样就不可以删除一部分内容了

<div  contenteditable="true" class="ant-input template-input">
这是一个模板输入<span class="replace_label" contenteditable="false" tpl_val="$1$">变量1</span>,
其他输入<span class="replace_label" contenteditable="false" tpl_val="$2$">变量2</span>
</div>

看看效果

20221027_154502[00_00_03--00_00_23].gif

实现技术点

1.在光标处插入文本

2.处理光标位置

实现编辑器第一个问题就是需要处理选区和光标,我们这个需求比较简单,基本上处理光标问题即可

需要用到Selection 对象和 Range 对象

Selection 对象表示用户选择的文本范围或插入符号的当前位置。

Selection 对象所对应的是用户所选择的 ranges(区域)就是 Range(框选变蓝的区域), 该接口表示一个包含节点与文本节点的一部分的文档片段。

SelectionRange的区别

Selection 可能包含多个Range,但通常我们只处理一个Range的区别,

Selection 主要可以处理焦点,即光标位置,Range处理选择的内容

在光标处插入文本

  1. 查找光标位置
  2. 插入内容
  3. 光标定位到插入位置后

主要用到的方法

Selection.getRangeAt(0) 获取Range

Selection.anchorNode 只读属性返回选区开始位置所属的节点(可以判断是否有选取)

Selection.collapseToEnd() 折叠选区结尾,如果是可编辑区域,会获取焦点,显示光标

Range.selectNode() 选择节点

代码示例:

      let selection = window.getSelection();
      let range = null;

      if(selection.anchorNode){
         range = selection.getRangeAt(0);
         range.deleteContents();
      }else{
        range = document.createRange();
        let ipt_node = this.$refs.edit_ipt;
        range.selectNodeContents(ipt_node);
        range.collapse();
      }
      let textNodes = this.replaceVal(val);
      let last = textNodes.lastChild;
      range.insertNode(textNodes);
      range.selectNode(last);

     selection.removeAllRanges();
     selection.addRange(range);
     selection.collapseToEnd();
     

处理光标位置

对于contenteditable="false"元素,有可能遇到这种情况,就光标在输入框外面了,而不是在后面

image.png

这种情况一般是后面没有内容的时候就会出问题,所以我们在后面放一个特殊符号&#xFEFF 这是一个0宽字符,不占宽度的

image.png

但是多了一个符号,删除时候要删2次,而且删了那个字符,也会出现光标问题,所以我们要处理删除的时候,同时删掉2个,就是判断光标位置的前一个字符是否是特殊字符,并且前面一个是我们的特殊元素,就一起删除

需要用到的方法

Range.endContainer 返回包含 Range 终点的节点。

Range.endOffset 返回代表 Range 结束位置在 Range.endContainer 中的偏移值的数字。

如果 endContainer 的 Node 类型为 Text, Comment,或 CDATASection,偏移值是 endContainer 节点开头到 Range 末尾的总字符个数。对其他类型的 Node , endOffset 指 endContainer 开头到 Range 末尾的总 Node 个数

代码示例:

    onKeydown(e){
      if(e.key == 'Backspace'){
        let selection = window.getSelection();
        const removeReplace = (last_child) => {
          //有可能有空的文本节点
          if(last_child.wholeText.length == 0){
            let last_child_prev = last_child.previousSibling;
            last_child.remove();
            last_child = last_child_prev;
          }
          if(last_child.wholeText.length == 1 && last_child.wholeText.charCodeAt(0) == 65279){
            let previousSibling = last_child.previousSibling;
            console.log(previousSibling);
            if(previousSibling && previousSibling.nodeType == Node.ELEMENT_NODE ){
              if(previousSibling.classList && previousSibling.classList.contains('replace_label')){
                previousSibling.remove();
                last_child.remove();
                return true;
              }
            }
          }

        }
        if(selection.anchorNode){
          let range = selection.getRangeAt(0);
          console.log(range);
          if(range.collapsed){
            let endOffset = range.endOffset;
            if(endOffset == 0){
              endOffset = 1;
            }
            if(range.endContainer.nodeType == Node.TEXT_NODE){
              //判断删除特殊符号
              if(removeReplace(range.endContainer,range)) {
                this.replaceToText()
                e.preventDefault();
              }
            }else{
              let last_child = range.endContainer.childNodes[endOffset-1];
              //判断删除特殊符号
              if(last_child.nodeType == Node.TEXT_NODE){
                if(removeReplace(last_child)){
                  this.replaceToText()
                  e.preventDefault();
                }
              }
            }
          }

        }
      }
    }

总结

对于富文本编辑器来说,需要处理的问题非常多,我们这么简单的需求,光标问题处理起来也比较麻烦