如何在按键事件发生后获取文本光标位置?

28
我正在编写一个语法高亮器。该高亮器应该在输入文本和使用箭头键导航时立即更新高亮显示。
我面临的问题是,当触发“keypress”事件时,仍然会通过 window.getSelection() 获取文本光标的旧位置。
示例:

function handleKeyEvent(evt) {
  console.log(evt.type, window.getSelection().getRangeAt(0).startOffset);
}

var div = document.querySelector("div");
div.addEventListener("keydown", handleKeyEvent);
div.addEventListener("keypress", handleKeyEvent);
div.addEventListener("input", handleKeyEvent);
div.addEventListener("keyup", handleKeyEvent);
<div contenteditable="true">f<span class="highlight">oo</span></div>

在这个示例中,将插入符号放置在单词“foo”之前,然后按下(右箭头键)。

在您喜欢的开发工具的控制台中,您将看到以下内容:

keydown 0
keypress 0
keyup 1

那个 0 除了 keypress 外显然是旧的插入符位置。如果你按住 稍微久一点,你会得到类似下面的内容:

keydown 0
keypress 0
keydown 1
keypress 1
keydown 1
keypress 1
keydown 2
keypress 2
keyup 2

我想要获取新的插入符位置,就像在“keyup”或“input”事件中一样。然而,“keyup”事件触发太晚了(我想在按下键时突出语法),而“input”事件仅在实际输入时触发(但不会产生任何输入)。

是否有一个事件是在插入符位置改变时触发而不仅仅是在输入时触发?或者我必须计算文本光标的位置,如果是这样,如何计算?(我假设当文本换行并且您按下键时,情况可能会变得非常复杂。)


1
我认为你在使用keypress事件时会遇到另一个问题。当按下箭头键时,Chrome似乎不会触发该事件(Firefox会)。这里有一个旧的chromium bug,状态为WontFix,描述了这个问题:https://bugs.chromium.org/p/chromium/issues/detail?id=2606 - Alex
谢谢你的提示!没错,keypress事件不会被触发,但是它显然已经被标记为过时了。正如我现在所看到的,规范说明人们应该使用beforeinput事件。而且keydown也会持续触发。虽然所有这些事件都会过早地触发。我现在已经在UI事件规范上提交了一个问题,希望能得到一个适当的事件来解决这个问题。 - Sebastian Zartner
1
选定光标位置等相关操作在各种浏览器上仍然很麻烦。虽然我已经有一段时间没用过了,但是 rangy 在处理光标相关的事情方面非常可靠。 - Geert-Jan
3个回答

22

您可以使用setTimeout来异步处理keydown事件:

function handleKeyEvent(evt) {
    setTimeout(function () {
        console.log(evt.type, window.getSelection().getRangeAt(0).startOffset);
    }, 0);
}

var div = document.querySelector("div");
div.addEventListener("keydown", handleKeyEvent);
<div contenteditable="true">This is some text</div>

这种方法解决了关键的处理问题。在你的例子中,你还有一个 span 元素位于 div 内部,它会改变返回的位置值。

window.getSelection().getRangeAt(0).startOffset

1
使用setTimeout()的方式看起来有点像hack,但它确实有效。所以,谢谢你!如果未来几天没有人找到更好的解决方案,我会接受你的答案。我知道在位置值中进行了更改,我只是在我的示例中提供了一些简化的代码。getRangeAt(0)返回的范围还提供了startContainer属性中的文本节点,startOffset也是指向该节点,所以这不是问题。 - Sebastian Zartner
1
调用setTimeout(fn, 0)是一个众所周知的技术,用于在当前调用栈完成后运行代码。您可以查看以下文章:为什么有时使用setTimeout(fn, 0)很有用?设置0秒的延迟意味着什么?JavaScript教程; 还有这个有趣的视频 - ConnorsFan
抱歉,这在我的情况下不起作用。我正在使用Firefox 94。将光标移动到最左边,然后按1次向右箭头。索引仍然显示为0,而光标位于索引1上。 - Melroy van den Berg

2
这是一种使用“keydown”事件来修正位置的解决方案:

function handleKeyEvent(evt) {
  var caretPos = window.getSelection().getRangeAt(0).startOffset;

  if (evt.type === "keydown") {
    switch(evt.key) {
      case "ArrowRight":
        if (caretPos < evt.target.innerText.length - 1) {
          caretPos++;
        }
        break;

      case "ArrowLeft":
        if (caretPos > 0) {
          caretPos--;
        }
        break;

      case "ArrowUp":
      case "Home":
        caretPos = 0;
        break;

      case "ArrowDown":
      case "End":
        caretPos = evt.target.innerText.length;
        break;

      default:
        return;
    }
  }
  console.log(caretPos);
}

var div = document.querySelector("div");
div.addEventListener("keydown", handleKeyEvent);
div.addEventListener("input", handleKeyEvent);
<div contenteditable="true">f<span class="highlight">oo</span></div>

不幸的是,这个解决方案本身有几个缺陷:
  • 当在像示例中的<span>这样的子元素内部时,它不能提供正确的startOffsetstartContainer
  • 它无法处理多行。
  • 它不能处理所有的导航选项。例如,通过Ctrl+/逐字跳跃没有被识别。

可能还有更多我没想到的问题。虽然处理所有这些问题是可能的,但会使实现变得非常复杂。因此,简单的setTimeout(..., 0)解决方案由ConnorsFan提供,直到有一个关于插入符位置更改的事件为止,肯定是更可取的。


1

有人在我的请求关于光标位置更改的事件上提到,还有一个selectionchange事件,每次选择发生变化时都会在文档中触发。

这样就可以通过调用window.getSelection()来获取正确的光标位置。

示例:

function handleSelectionChange(evt) {
    console.log(evt.type, window.getSelection().getRangeAt(0));
}

document.addEventListener("selectionchange", handleSelectionChange);
<div contenteditable="true">f<span class="highlight">oo</span></div>


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