Reputation: 20125
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).)
Upvotes: 29
Views: 29749
Reputation: 20125
Someone just mentioned on my request for an event for caret position changes that there is also a selectionchange
event, which is fired at the document everytime the selection has changed.
This then allows to get the correct cursor position by calling window.getSelection()
.
Example:
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>
Upvotes: 1
Reputation: 20125
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:
<span>
in the example, it doesn't provide the correct startOffset
nor the correct startContainer
.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.
Upvotes: 2
Reputation: 73761
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
Upvotes: 23