我可以使用什么来使用多个文本节点可靠地获取 Javascript 中单击的字符偏移量?

What can I use to reliably get a character offset of a click in Javascript using multiple nodes of text?

我正在使用 Window.getSelection 在用户单击某些文本时获取用户单击位置的字符偏移量。当只有一个文本节点时,这非常有效,并且关于如何执行此操作的 SO 上有很多很好的答案。

但是,当我有两个或多个文本节点彼此相邻呈现时,我遇到了问题。 Here's a fiddle that replicates the problem,但我将在此处逐步介绍:

用例:

我正在构建一个文本编辑器并使用 Javascript 控制 DOM 来响应用户按键,而不是使用 contentEditable 容器。我想跟踪(并显示)用户的“光标”在文本中的位置(即,如果他们开始输入,文本将输入的位置),以及让他们单击文本中的任意位置以手动设置光标到他们点击的地方。

HTML:

<p class="sentence">
  <span class="word" index="0">There </span><span class="word" index="1">is </span><span class="word" index="2">a </span><span class="word" index="3">big, </span><span class="word" index="4">wonderful </span><span class="word" index="5">world.</span>
</p>

Javascript:

$('.word').click(function() {
  let word_index = $(this).attr('index');
  
  let selection  = window.getSelection();
  let char_index = selection.focusOffset;

  console.log('Clicked to set a new cursor @');
  console.log('Word index = ' + word_index.toString() + 
             ' / char index = ' + char_index.toString() );
});

简而言之,这段代码打印了一个 word index 和一个 character index 来表示光标在文本中的位置,就好像它是一个可编辑的输入一样。如果你点击“There”中“T”的左半部分,它会打印

Clicked to set a new cursor @
Word index = 0
/ char index = 0

单击“T”的右侧(这样光标将放在它之后但“h”之前)将打印相同的单词索引,但字符索引为 1,因为光标现在位于放置一个字符,依此类推。

这也很好用...除了单击任何单词的第一个字符的左半部分(第一个字符除外)。单击“is”中“i”的左半部分(在单词索引 1、字符索引 0 处设置插入符号),而是打印单词索引 1(正确)和字符索引 6(前一个单词的长度)。

当有多个文本节点彼此相邻时,Window.getSelection(或更具体地说,selection.focusOffset)是否不是计算这种字符偏移的正确方法?我需要使用其他库或方法吗?

该问题的一个“解决方法”是在每个单词周围应用边距以在它们之间放置一个不可点击的间隙。在这种情况下,单击“i”的左半部分会给出正确的 word/char 索引(1,0 而不是 1,6),但会产生空白的副作用,如果用户碰巧不小心,则不会发生任何事情单击那里(此应用程序中的严重副作用,因此它不是真正可行的“修复”)。看起来至少 3px 的边距总是 returns 正确的值,而 2px 或更少的边距总是 returns 不正确的值。

我正在 Chrome 中进行测试,因为我最终会构建一个 Electron 应用程序,所以我想我真的只需要它在 Blink 中工作,但如果解决方案是Web 版本也与浏览器无关。


解决方案更新:

我能够通过实施以下两个额外的防护措施来实现这一点:

// If we hit an overlap with another bit of selectable
// text, we zero out the cursor offset to avoid using
// that word's offset -- the left edge of a textnode
// is the only place where this happens, so we know
// the correct cursor offset is 0.
if ($(this).text() !== selection.focusNode.wholeText)   {
  char_index = 0;
}

// There's also a small clickable area on the right
// side of the final character in a word that would
// result in a cursor at the end of that word (often
// where the delimiting space is). We simply shift
// the cursor's reference from end-of-that-word to
// start-of-the-next word, which are the same to the
// user, but makes more sense for keeping proper
// word individuation.
if (char_index === $(this).text().length) {
  word_index += 1;
  char_index  = 0;
}

要使其跨浏览器还有很多工作要做,但是 here is an updated fiddle 使用这些调整在 Chrome 中获得所需的值。

默认行为取决于浏览器,因此您需要进行一些检查才能获得一致的结果。

这是一个简单的例子,它比较 event.target 元素与 selection.focusNode 返回的文本节点的 parentElement,如果不匹配,它会检查焦点节点的兄弟节点匹配并相应地调整偏移量。

您需要使它更健壮。您还需要处理方向性(选择从左到右会产生相反的结果 focus and anchor nodes to selecting right to left). You might also look at Selection.containsNode()

function handleSelection(e) {
  const selection = window.getSelection();
  
  const fNode = selection.focusNode;
  
  const fElement = fNode.parentElement;
  const tElement = e.target;
  
  let word_index = tElement.dataset.index;
  let char_index = selection.focusOffset;
  
  if (!fElement.isEqualNode(tElement) 
    && fElement.nextElementSibling.isEqualNode(tElement)) {
    char_index=0;
  }
  
  console.log(
    'fNode: ', fNode.parentElement.innerText,
    ' tNode: ', tElement.innerText,
    ' char: ', char_index,
    ' word: ', word_index
  );

}

const words = document.querySelectorAll('.word');
words.forEach((w, i) => (
  w.dataset.index = i+1, 
  w.addEventListener('click', handleSelection)));
.word {
  border: 1px solid gray;
  font-size: 2rem;
}
<p class="sentence">
  <span class="word">Hello </span><span class="word">wonderful</span><span class="word"> world.</span>
</p>