I’m having a challenge getting an accurate starting position in a string with mixed content (HTML and text). The content is entered by users into a contenteditable div. I can’t use textareas because HTML tags can be added to the content and textareas don’t accept anything but text.
In the app, users enter text and then they are given the opportunity to select text for highlighting (the app adds <span class="hilite"></span>
tags to the selected text) and "tags" (<span>{{selected text}}<sup>{{tagname}}</sup></span>
tags) so that a string can look like this:
a very <span class="hilite">unhappy</span> person <span>made life very difficult<sup>problem</sup></span> for another person
The
items appear to be added by the browser (a unique feature of contenteditable divs?) any time there is more than one space sequentially and this can appear as "
" (regular space first) or "
" (regular space last) or a series of
spaces with a regular space at the beginning or the end.
Because this is user driven input, it’s never known how many tags or
spaces will be found in a string. Either way, the goal is to count every character in the literal html string up to the beginning of the selected text.
For example, if the user wants to "tag" the second instance of "person", the user highlights the word, then a right click will pop up a modal with the selected text. At the moment of the right click I need to get an accurate position of the selected text in the context of the whole string as it exists at the time. And, of course, I won’t know how many spans, sups or
elements there are or how many instances of "person" there might be.
Here’s my basic event handler for the right click (using jQuery):
$('.editableDiv').on('contextmenu', function(e) {
e.preventDefault();
let fullString = $(this).html();
let selectedText = window.getSelection();
let textStart = {{what goes here is the problem}}
$.ajax(
{{send arguments to server,
do stuff,
return the amended string
replace the contents of the $(this) with the returned string}}
);
});
The specific problem is how do I set the value of textStart accurately.
JavaScript’s string.indexOf(person)
won’t work because it only finds the first instance of person and I’ll never know how many instances there will be since the user is entering the text and selecting the text to manipulate.
I’ve also tried traversing the DOM like this (called with selectedText from above as selection
):
function findTheStartPosition(selection) {
let range = selection.getRangeAt(0);
let startContainer = range.startContainer;
let start = 0;
for (let node of Array.from(startContainer.parentElement.childNodes)) {
if (node === startContainer) {
break;
}
if (node.nodeType === Node.TEXT_NODE) {
start += node.textContent.length;
} else if (node.nodeType === Node.ELEMENT_NODE) {
let tempDiv = document.createElement('div');
tempDiv.appendChild(node.cloneNode(true));
start += tempDiv.innerHTML.length;
}
}
start += range.startOffset;
return start;
}
And that works well, except when there are
elements because range.startOffset
counts all
characters as single spaces and thus the position is inaccurate.
I’m trying to avoid stripping the main string of all tags and spaces because it will require keeping track of what was stripped and the length of each item stripped (including a count of 6 characters for every
) and then having to rebuild the string to preserve the tags (but not the superfluous spaces)….nightmare.
All I need is a simple, reliable way to get the start position of the user selected text in a string.
2
Answers
I suggest either:
ProseMirror is a (framework for building an) editor that uses
contenteditable
under the hood.You might be able to just modify the ProseMirror basic example to fit your needs. The selection start and end are accessed via
let {from, to} = state.selection
. (Take a look at the tooltips example.) These examples support inserting HTML objects like images and<HR>
‘s.I think ProseMirror has several advantages over a plain
contenteditable
<div>
. But they are not without costs like added complexity and a steep learning curve. (For complex, niche use cases you may have to define your own document grammar.)So another option is to simply emulate ProseMirror’s method:
contenteditable
<div>
. This avoids the problem you are facing with
being inserted for spaces. (Also I would be surprised ifcontenteditable
behaves exactly the same on all platforms/browsers).As discussed in the comments, what you really need is replacing the
s before selection with regular spaces.You didn’t get my suggestion. I didn’t mean re-traversing each time the user enters a new symbol. That would make your website really slow. I meant re-traversing when the user selects something. And
startOffset
is only used to replace
s within thestartContainer
.Anyway, here’s the implementation for you to test.
You no longer need that algorithm from your question, since the traversal is performed in function
handleNbspsBefore
.