I'm using Window.getSelection to get the character offset of where a user clicks, when that user clicks on some text. This works great when there is a single text node, and there are a lot of great answers on SO about how to do so.
However, I'm having a problem when I have two or more text nodes rendered next to each other. Here's a fiddle that replicates the problem, but I'll walk through it here:
Use-case:
I'm building a text editor and controlling the DOM with Javascript that reacts to user keypresses, instead of using a contentEditable container. I would like to track (and show) where in the text a user's "cursor" is (i.e. where the text would be entered if they were to start typing), as well as let them click anywhere in the text to manually set their cursor to where they clicked.
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() );
});
In short, this code prints a word index
and a character index
for where a cursor would be placed in the text, as if it were an editable input. If you clicked on the left half of the "T" in "There", it'd print
Clicked to set a new cursor @
Word index = 0
/ char index = 0
Clicking on the right side of "T" (such that a cursor would be placed after it but before the "h") would print the same word index, but a char index of 1, since the cursor is now placed one character over, and so on.
This also works great... except for clicking on the left-half of the first character of any word (other than the first). Clicking on the left-half of "i" in "is" (to set a caret at word index 1, char index 0) instead prints word index 1 (correct) and char index 6 (the length of the previous word).
Is Window.getSelection
(or more specifically, selection.focusOffset
) not the correct way to calculate this kind of character offset when there are multiple text nodes next to each other? Is there another library or method I need to use instead?
One "fix" for the problem is to apply a margin around each word to put a non-clickable gap between them. In this case, clicking the left-half of "i" gives the correct word/char index (1,0 instead of 1,6), but has the side effect of having whitespace where nothing happens if a user happens to accidentally click there (a serious side effect in this application, so it isn't really a viable "fix"). It looks like a margin of at least 3px always returns the correct values, while a margin of 2px or less always returns the incorrect values.
I'm testing in Chrome because I'll eventually be building into an Electron app, so I guess I only really need it to work in Blink, but it'd be wonderful if a solution was browser-agnostic for a web release, too.
Solution update:
I was able to get this working by implementing the following two additional guardrails:
// 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;
}
There's more work to make it cross-browser, but here is an updated fiddle that uses these adjustments to get the desired values in Chrome.