Here's an attempt at a general solution that allows crossing element boundaries. Probably doesn't mix well with images, etc., nor non left-to-right text, but should be OK with simple cases.
First, here's a very general function for walking text nodes in the subtree defined by a Range object, from left to right. This will give us all the text we need as pieces:
function walkRange(range) {
let ranges = [];
let el = range.startContainer;
let elsToVisit = true;
while (elsToVisit) {
let startOffset = el == range.startContainer ? range.startOffset : 0;
let endOffset = el == range.endContainer ? range.endOffset : el.textContent.length;
let r = document.createRange();
r.setStart(el, startOffset);
r.setEnd(el, endOffset);
ranges.push(r);
/// Move to the next text container in the tree order
elsToVisit = false;
while (!elsToVisit && el != range.endContainer) {
let nextEl = getFirstTextNode(el.nextSibling);
if (nextEl) {
el = nextEl;
elsToVisit = true;
}
else {
if (el.nextSibling) el = el.nextSibling;
else if (el.parentNode) el = el.parentNode;
else break;
}
}
}
return ranges;
}
That makes use of this utility function for getting the first (leftmost) text node in a subtree:
function getFirstTextNode(el) {
/// Degenerate cases: either el is null, or el is already a text node
if (!el) return null;
if (el.nodeType == 3) return el;
for (let child of el.childNodes) {
if (child.nodeType == 3) {
return child;
}
else {
let textNode = getFirstTextNode(child);
if (textNode !== null) return textNode;
}
}
return null;
}
Once you've called walkRanges
, you can just use surroundContents
on what it returns to actually do the highlighting/marking. Here it is in a function:
function highlight(range, className) {
range = range.getRangeAt ? range.getRangeAt(0) : range;
for (let r of walkRange(range)) {
let mark = document.createElement('mark');
mark.className = className;
r.surroundContents(mark);
}
}
and to unhighlight (assuming you used a unique class name for the highlight):
function unhighlight(sel) {
document.querySelectorAll(sel).forEach(el => el.replaceWith(...el.childNodes));
}
Example usage:
highlight(document.getSelection(), 'mySelectionClassName');
unhighlight('.mySelectionClassName')