修复在替换 <div contenteditable="true"> 的 innerHTML 时,光标位置的问题。

7
如何在每次按键时更改 <div id="richTextBox" contenteditable="true"></div> 的 innerHTML 时,保持光标位置不变?替换 innerHTML 会破坏光标位置。
我更改 innerHTML 的原因是 添加了 <span> 标签, 这是代码高亮程序的一部分。Span 标签允许我放置正确的颜色高亮显示。
我暂时使用 StackOverflow 答案中的以下代码作为临时方法,但它有一个重大的漏洞。如果按下回车键,则光标停留在旧位置或跳到随机位置。这是因为算法计算光标从开头算起的字符数,但它不将 HTML 标记或换行符视为字符。richTextBox 插入 <br> 来创建换行符。
修复思路:
  • 修复以下代码? 请参见Fiddle
  • 用更简单的代码替换?我尝试过很多简单的东西,涉及 window.getSelection()document.createRange(),但我无法使它们正常工作。
  • 替换没有此错误的 richTextBox 库或模块?

截图

richTextBox 在 JSFiddle 中渲染的屏幕截图

// Credit to Liam (Stack Overflow)
// https://dev59.com/Jm025IYBdhLWcg3wCxVN#41034697
class Cursor {
  static getCurrentCursorPosition(parentElement) {
    var selection = window.getSelection(),
      charCount = -1,
      node;

    if (selection.focusNode) {
      if (Cursor._isChildOf(selection.focusNode, parentElement)) {
        node = selection.focusNode; 
        charCount = selection.focusOffset;

        while (node) {
          if (node === parentElement) {
            break;
          }

          if (node.previousSibling) {
            node = node.previousSibling;
            charCount += node.textContent.length;
          } else {
            node = node.parentNode;
            if (node === null) {
              break;
            }
          }
        }
      }
    }

    return charCount;
  }

  static setCurrentCursorPosition(chars, element) {
    if (chars >= 0) {
      var selection = window.getSelection();

      let range = Cursor._createRange(element, { count: chars });

      if (range) {
        range.collapse(false);
        selection.removeAllRanges();
        selection.addRange(range);
      }
    }
  }

  static _createRange(node, chars, range) {
    if (!range) {
      range = document.createRange()
      range.selectNode(node);
      range.setStart(node, 0);
    }

    if (chars.count === 0) {
      range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
      if (node.nodeType === Node.TEXT_NODE) {
        if (node.textContent.length < chars.count) {
          chars.count -= node.textContent.length;
        } else {
          range.setEnd(node, chars.count);
          chars.count = 0;
        }
      } else {
        for (var lp = 0; lp < node.childNodes.length; lp++) {
          range = Cursor._createRange(node.childNodes[lp], chars, range);

          if (chars.count === 0) {
          break;
          }
        }
      }
    } 

    return range;
  }

  static _isChildOf(node, parentElement) {
    while (node !== null) {
      if (node === parentElement) {
        return true;
      }
      node = node.parentNode;
    }

    return false;
  }
}

window.addEventListener('DOMContentLoaded', (e) => {
  let richText = document.getElementById('rich-text');

  richText.addEventListener('input', function(e) {
    let offset = Cursor.getCurrentCursorPosition(richText);
    // Pretend we do stuff with innerHTML here. The innerHTML will end up getting replaced with slightly changed code.
    let s = richText.innerHTML;
    richText.innerHTML = "";
    richText.innerHTML = s;
    Cursor.setCurrentCursorPosition(offset, richText);
    richText.focus(); // blinks the cursor
  });
});
body {
  margin: 1em;
}

#rich-text {
  width: 100%;
  height: 450px;
  border: 1px solid black;
  cursor: text;
  overflow: scroll;
  resize: both;
  /* in Chrome, must have display: inline-block for contenteditable=true to prevent it from adding <div> <p> and <span> when you type. */
  display: inline-block;
}
<p>
Click somewhere in the middle of line 1. Hit enter. Start typing. Cursor is in the wrong place.
</p>

<p>
Reset. Click somewhere in the middle of line 1. Hit enter. Hit enter again. Cursor goes to some random place.
</p>

<div id="rich-text" contenteditable="true">Testing 123<br />Testing 456</div>

浏览器

Windows 7, Google Chrome v83


1
你有没有考虑使用像jQuery的$(selector).text()这样的解析器来代替innerHTML?它会去掉标签。使用它可能会避免尝试计算你插入或删除了多少个标签的头痛... 它只会提供文本内容,而不是HTML的长度。 - joshstrike
@joshstrike 好主意。我会试一下的。 - RedDragonWebDesign
1个回答

4
问题似乎在于添加新行会添加<br>,但由于您仍然在父元素中,以前的DOM子元素没有被考虑在内,而selection.focusOffset仅给出4的值。
当您删除并重新添加innerHtml时,将有所帮助,因为它正在被剥离。 在Fiddle的第100行末尾添加+ "\n"即可。
但是,您的主要问题是从其他StackOverflow问题中复制的getCurrentCursorPosition实际上不起作用。
我建议您查看此问题的其他答案:Get contentEditable caret index position,并console.log他们的输出,看看哪个最适合您的边缘情况。 如果您不想自己编写,则Caret.jsAt.js编辑器库的一部分)会很有用。

谢谢!修复似乎有效。然而,新的isChildOf函数会抛出Uncaught ReferenceError: isChildOf未定义。有什么想法吗? - RedDragonWebDesign
我不知道代码在抛出错误的情况下是如何工作的。我尝试注释掉代码的一部分,理论上它不应该被执行,因为有语法错误,但是注释掉它会破坏你的修复。我对发生了什么感到困惑。 - RedDragonWebDesign
啊,是的,对我来说小提琴没有设置好 - 我没有收到那个错误,但我看到你可以在那里引起一个错误,然后中断并取消所有光标移动代码,这意味着它的行为就像通常一样,可能不是你想要的! - Luke Storry
@LukeStorry 其他答案都不起作用,所以这里的解决方案仍然非常感激。说实话,我不明白Caret.js如何解决问题 - 从您提供的链接中的演示中,它们在绝对屏幕坐标处有一个光标,而不是字符计数。也许我只是错过了什么 - 假设您获得了这些值,如何将光标放置在此位置? - bgplaya
抱歉,我现在看到了示例,但是这两个项目似乎已经被放弃了,也没有其他的替代方案。如果您知道任何替代方案,我将非常感激。 - bgplaya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接