contenteditable DIV完美解决maxlength问题

1,175 阅读3分钟

contenteditable 是一个可以使DIV可编辑的属性,可以用它来制作富文本编辑器。也许你会需要一个像 textarea 一样的 maxlength 属性,如果你没有做过,可能会觉得很简单,超过一定字数截掉不就行了。然而,并没有那么简单,下面是我在做这个的过程中遇到的问题:

  1. 超过字数替换event.target.innerHTML内容,出现光标回到开头,应该可以通过一点手段使它回到最后,但是如果是中间就麻烦了,所以放弃了这个方案。
  2. 使用document.execCommand("insertHTML")替换内容,可以使光标保持在最后,但是光标原来在中间也麻烦,还有一个问题,替换内容会使滚动条回到顶部,虽然可以通过代码滚回原来的位置,但是感觉体验还是不好。
  3. 怎么计算要截去哪些字符是个问题。innerHTML是带标签的,要截去内部文字比较麻烦,而且会存在一些转义字符类似<>等。
  4. 字符达到限制,截去的都是尾部的内容,中间继续输入内容会保留。按照 textarea 的实现,当字符超过,不能继续输入是不是应该更合理?

既然替换内容这么多问题,那是不是可以通过禁止键盘输入,这个就要考虑KeyboardEvent事件的。首先使用keydown事件,判断innerText超过字数,阻止默认事件,这个太粗暴了,把所有按键都阻止掉了,删除也不行。难度要筛选对应keyCode去阻止?这个要包含全部太繁琐,也容易出bug。

keypress

keypress事件在键盘输入产生字符时才会触发,用这个去阻止默认事件。

const onKeypress = (event) => {
  if (event.target.innerText.length >= props.maxlength) {
    event.preventDefault();
  }
};

compositionend

keypress可以禁掉英文输入,但是中文拼音输入的时候不能阻止。这个时候要考虑使用compositionend事件,MDN描述如下:

当文本段落的组成完成或取消时,compositionend 事件将被触发 (具有特殊字符的触发,需要一系列键和其他输入,如语音识别或移动中的字词建议)。

不过这个事件并不能阻止默认事件(只在谷歌试过),所以还是要当输入内容时,使用截去字符的方法,可是上面说到的替换文本问题,所以要考虑能不能取部分内容替换。execCommand有个delete命令,可以删除选中内容,要解决的是如何选中内容。经过测试,创建Range对象可以选中文案,而且选区创建在当前元素内或者段落。如图:

image.png

代码如下:

const range = document.createRange();
const sel = window.getSelection();
const node = sel.anchorNode;
range.selectNodeContents(node);
sel.removeAllRanges();
sel.addRange(range);

然后删除内容,重新添加新的内容,但是直接这样修改也有问题,新添加的内容在哪呢?每次修改后光标都在这行最后,好像不满足要求。解决这个问题,最简单的问题是把选区移动到内容添加处,然后把这部分内容删除掉后面多余的。

image.png

上面增加两行代码:

const range = document.createRange();
const sel = window.getSelection();
const offset = sel.anchorOffset; // 光标偏移位置
const node = sel.anchorNode;
range.selectNodeContents(node);
sel.removeAllRanges();
sel.addRange(range);
sel.extend(node, offset); // 移动选区到原来光标位置

然后就是替换内容了。事件内完整代码:

const onCompositionend = (event) => {
  const diff = event.target.innerText.length - props.maxlength;
  if (diff > 0) {
    const range = document.createRange();
    const sel = window.getSelection();
    const offset = sel.anchorOffset;
    const node = sel.anchorNode;
    const text = node.textContent;
    range.selectNodeContents(node);
    sel.removeAllRanges();
    sel.addRange(range);
    sel.extend(node, offset);
    document.execCommand('delete', false);
    document.execCommand(
      'insertText',
      false,
      text.substring(0, offset - diff)
    );
  }
};

其中火狐removeAllRangesaddRange可能会冲突,猜测removeAllRanges是异步执行的,导致addRange之后执行删除,可添加setTimeout处理。

这个方案完美解决了替换内容后光标问题、滚动条问题、字符统计问题。不过execCommand这个语法被弃用了,目前还没有较好的替代方案。

完整代码查看:[点这里]