How to get text cursor position after keypress event happened?

20,407

Solution 1

You can use setTimeout to process the keydown event asynchronously:

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>

That method addresses the key processing problem. In your example, you also have a span element inside of the div, which alters the position value returned by

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

Solution 2

Here's a solution correcting the position using the 'keydown' event:

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>

Unfortunately this solution as is has several flaws:

  • When inside a child element like the <span> in the example, it doesn't provide the correct startOffset nor the correct startContainer.
  • It cannot handle multiple lines.
  • It doesn't handle all navigation options. E.g. jumping word-wise via Ctrl+/ is not recognized.

And there are probably more issues I didn't think of. While it would be possible to handle all those issues, it makes the implementation very complex. So the simple setTimeout(..., 0) solution provided by ConnorsFan is definitely preferable until there is an event for caret position changes.

Share:
20,407

Related videos on Youtube

Sebastian Zartner
Author by

Sebastian Zartner

I was a member of the Firebug Working Group. I like to program in JavaScript, ColdFusion and PHP. Currently I am working on improving the Firefox DevTools and a Firefox extension called Regular Expressions Tester (TRex). Furthermore I'm helping to document on the Mozilla Developer Network (MDN) and giving some input to the Inkscape team. I am also an invited expert of the W3C, providing some feedback in regard of the CSS standards and helping to shape the specifications.

Updated on August 14, 2020

Comments

  • Sebastian Zartner
    Sebastian Zartner over 3 years

    I am writing a syntax highlighter. The highlighter should update the highlighting immediately while entering text and navigating with the arrow keys.

    The problem I'm facing is that when the 'keypress' event is fired, you still get the old position of the text cursor via window.getSelection().

    Example:

    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>

    In the example, place the caret before the word 'foo', then press (the Right Arrow key).

    Within the console of your favorite DevTool you'll see the following:

    keydown 0
    keypress 0
    keyup 1
    

    That 0 besides keypress is obviously the old caret position. If you hold down a bit longer, you'll get something like this:

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

    What I want to get is the new caret position like I would get it for 'keyup' or 'input'. Though 'keyup' is fired too late (I want to highlight the syntax while the key is pressed down) and 'input' is only fired when there is actually some input (but doesn't produce any input).

    Is there an event that is fired after the caret position has changed and not only on input? Or do I have to calculate the position of the text cursor and if so, how? (I assume this can get quite complicated when the text wraps and you press (the Down Arrow key).)

    • Alex
      Alex over 7 years
      I think you will have another problem using the keypress event. Chrome doesn't seem to fire the event when pressing an arrow key (Firefox does). There is an old chromium bug in status WontFix describing this: bugs.chromium.org/p/chromium/issues/detail?id=2606
    • Sebastian Zartner
      Sebastian Zartner over 7 years
      Thank you for the hint! Right, the keypress event is not fired, but it's obviously marked as obsolete, anyway. As I saw now, the specification states that people are advised to use the beforeinput event instead. And keydown is also fired continuously. Though all those events are fired too early. I've now filed an issue on the UI Events spec. in the hope to get a proper event for this.
    • Geert-Jan
      Geert-Jan over 7 years
      Cursor positions / selecting etc are still a pita to support on all browsers. It's been a while that i needed this but rangy is very solid with cursor related stuff..
  • Sebastian Zartner
    Sebastian Zartner over 7 years
    Using setTimeout() this way looks rather a hack but it works. So, thanks for that! If nobody finds a nicer solution the next days, I'll accept your answer. I'm aware of the altering in the position value, I just provided some simplified code in my example. The range returned by getRangeAt(0) also provides the text node within the startContainer property, which the startOffset refers to, so that's not a problem.
  • ConnorsFan
    ConnorsFan over 7 years
    Calling setTimeout(fn, 0) is a well-known technique for running code after the current call stack has completed. You can see these articles: Why is setTimeout(fn, 0) sometimes useful?, What does setTimeout with a 0ms delay do?, Javascript tutorial; and this entertaining video.
  • Melroy van den Berg
    Melroy van den Berg over 2 years
    It doesn't work in my case sorry. I'm using Firefox 94. Moving the cursor all to the left. And than press 1x right. The index still says 0, while the cursor is on index 1.