How To Wrap / Surround Highlighted Text With An Element

Viewed 35233

I want to wrap a selected text in a div container with span, is it possible?

A user will select a text and will click a button, on button click event I want to wrap that selected text with span element. I can get the selected text using window.getSelection() but how to know its exact position in DOM structure?

7 Answers

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')

Following works across multiple dom elements

function highlightSelection() {       
    let selection= window.getSelection().getRangeAt(0);
    let selectedContent = selection.extractContents();
    var span= document.createElement("span");
    span.style.backgroundColor = "lightpink";
    span.appendChild(selectedContent);
    selection.insertNode(span);
}
Make your <b>selection across multiple</b> elements <strike>and then click highlight</strike> button.

<button onclick="highlightSelection();">Highlight</button>

Related