Skip to content

Instantly share code, notes, and snippets.

@richkuz
Created May 25, 2023 01:54
Show Gist options
  • Save richkuz/2f9ab6defa7a9b711d5772ae29b7bfe3 to your computer and use it in GitHub Desktop.
Save richkuz/2f9ab6defa7a9b711d5772ae29b7bfe3 to your computer and use it in GitHub Desktop.
JavaScript to select a range of text in a paragraph excluding HTML markup
// Given an element, select a range of text given a start and end character position offset relative
// to the element's innerText, not counting any HTML markup in the element.
// Partial credit: https://stackoverflow.com/questions/16662393/insert-html-into-text-node-with-javascript
function injectMarkerIntoTextNode(node, startIndex) {
// startIndex is beginning location of the text to inject a span
let parentNode = node.parentNode;
var s = node.nodeValue;
var beforePart = s.substring(0, startIndex);
var afterPart = s.substring(startIndex);
// replace the text node with the new nodes
var textNode = document.createTextNode(beforePart);
parentNode.replaceChild(textNode, node);
// create a span node and add it to the parent immediately after the first text node
var spanNode = document.createElement("span");
spanNode.className = "SelectionMarker";
//spanNode.innerHTML = 'MARKER';
parentNode.insertBefore(spanNode, textNode.nextSibling);
// create a text node for the text after the highlight and add it after the span node
textNode = document.createTextNode(afterPart);
parentNode.insertBefore(textNode, spanNode.nextSibling);
return spanNode;
}
// Depth-first search all child elements, reading text chars until we find
// a text node within range of the desired startTextOffset, then inject a marker span element.
function injectMarkerRecursively(el, startTextOffset, state = { charsRead: 0 }) {
// Recurse through all descendents
for (var i = 0; i < el.childNodes.length; i++) {
var child = el.childNodes[i];
let injectedMarker = injectMarkerRecursively(child, startTextOffset, state);
if (injectedMarker) {
// Done!
return injectedMarker;
}
if (child.nodeType === Node.TEXT_NODE) {
let nodeValueLen = child.nodeValue.length;
if (startTextOffset <= state.charsRead + nodeValueLen) {
// Inject a marker inside this text node
return injectMarkerIntoTextNode(child, startTextOffset - state.charsRead);
}
else {
// Advance the cursor to the end of this node and keep looking
state.charsRead += nodeValueLen;
}
}
}
// No marker injected yet, keep recursing...
return null;
}
function clearMarkers(el) {
const elements = el.getElementsByClassName('SelectionMarker');
while(elements.length > 0){
elements[0].parentNode.removeChild(elements[0]);
}
}
// Credit: http://roysharon.com/blog/37
function scrollIntoView(t) {
if (typeof(t) != 'object') return;
// if t is not an element node, we need to skip back until we find the
// previous element with which we can call scrollIntoView()
o = t;
while (o && o.nodeType != 1) o = o.previousSibling;
t = o || t.parentNode;
if (t) t.scrollIntoView();
}
// Given an element, select a range of text given a start and end character position offset relative
// to the element's innerText, not counting any HTML markup in the element.
function selectTextRange(el, startTextOffset, endTextOffset) {
clearMarkers(el);
// Inject span element markers at the specified positions
const injectedMarkerEnd = injectMarkerRecursively(document.querySelector('#output'), endTextOffset);
const injectedMarkerStart = injectMarkerRecursively(document.querySelector('#output'), startTextOffset);
// Set the range relative to the span element markers
let range = new Range();
range.setStartAfter(injectedMarkerStart);
range.setEndBefore(injectedMarkerEnd);
// Select the range
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);
scrollIntoView(injectedMarkerStart);
}
// Example usage:
selectTextRange(document.querySelector('#mydiv'), 200, 205);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment