【问题标题】:Fix cursor position when replacing innerHTML of <div contenteditable="true">替换 <div contenteditable="true"> 的 innerHTML 时修复光标位置
【发布时间】:2020-07-02 21:40:44
【问题描述】:

&lt;div id="richTextBox" contenteditable="true"&gt;&lt;/div&gt; 中键入时,如何将光标保持在正确的位置,该&lt;div id="richTextBox" contenteditable="true"&gt;&lt;/div&gt; 的innerHTML 在每次击键时都会发生变化?替换 innerHTML 的行为会弄乱光标位置。

我更改innerHTML 的原因是我添加了&lt;span&gt; 标签。它是代码高亮程序的一部分。 span 标签允许我放置正确的颜色亮点。

我目前正在使用 StackOverflow 答案中的以下代码作为创可贴,但它有一个重大错误。如果您按 enter,光标将停留在旧位置,或转到随机位置。那是因为算法从光标开始计算了多少个字符。但它不会将 HTML 标记或换行符算作字符。并且richTextBox插入&lt;br&gt;进行回车。

修复思路:

  • 修复以下代码?见Fiddle
  • 用更简单的代码替换?我尝试了一堆涉及window.getSelection()document.createRange() 的更简单的东西,但我无法让它发挥作用。
  • 替换为没有此错误的richTextBox 库或模块?

截图

// Credit to Liam (Stack Overflow)
// https://stackoverflow.com/a/41034697/3480193
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>

浏览器

谷歌浏览器 v83,Windows 7

【问题讨论】:

  • 您是否考虑过像 jquery 的 $(selector).text() 这样的解析器而不是 innerHTML?它剥离标签。使用它可能会省去尝试计算插入或删除多少标签的麻烦......它只会给出文本内容而不是 HTML 的长度。
  • @joshstrike 好主意。我会试试看。

标签: javascript richtextbox text-cursor


【解决方案1】:

问题似乎是添加新行添加了&lt;br&gt;,但是由于您仍在父元素中,因此不考虑以前的DOM子元素,并且selection.focusOffset仅给出了@987654326的值@。

innerHtml 的末尾添加一个换行符可能会有所帮助,因为当您删除并重新添加它时它会被剥离。 + "\n" 到 Fiddle 第 100 行的末尾就可以了。


不过,您的主要问题是您从另一个 StackOverflow 问题中复制的 getCurrentCursorPosition 实际上不起作用。

我建议您查看此问题的其他一些答案: Get contentEditable caret index positionconsole.log 他们输出什么,看看哪一个最适合你的边缘情况。

如果您不想自己编写,那么Caret.jsAt.js 编辑器库的一部分)会很有用。


【讨论】:

  • 谢谢!修复似乎有效。但是,新的 isChildOf 函数正在抛出 Uncaught ReferenceError: isChildOf is not defined。知道发生了什么吗?
  • 我想知道代码在抛出错误的情况下是如何工作的。我尝试注释掉理论上由于语法错误而不应执行的代码部分,但注释掉它会破坏您的修复。我对发生的事情感到很困惑。
  • 啊,是的,小提琴没有完全适合我 - 我没有收到那个错误,但我看到你可能会在那里导致错误,然后中断并取消所有光标移动代码,这意味着它的行为就像平常一样,可能不是你想要的!
  • @LukeStorry 没有一个真正有效的答案,所以这里的解决方案仍然非常受欢迎。 Tbh,我看不到 Caret.js 如何解决问题 - 从您提供的链接的演示中,他们在绝对屏幕坐标处有一个光标,而不是字符数。也许我只是遗漏了一些东西——假设你获得了这些值,你如何将光标放在这个位置?
  • 我的错,我现在确实看到了这些示例,但是这两个项目似乎已经被放弃了,并且没有任何替代方案。如果您知道,我将不胜感激。
猜你喜欢
  • 2015-05-29
  • 2012-07-29
  • 2011-11-16
  • 1970-01-01
  • 2011-04-16
  • 2011-08-01
  • 1970-01-01
  • 2010-11-13
  • 2013-04-18
相关资源
最近更新 更多