47

I would like to highlight (apply css to) a certain text range, denoted by its start and end position. This is more diffucult than it seems, since there may be other tags within the text, that need to be ignored.

Example:

<div>abcd<em>efg</em>hij</div>

highlight(2, 6) needs to highlight "cdef" without removing the tag.

I have tried already using a TextRange object, but without success.

Thanks in advance!

3
  • Can you strip the tags in a temporary string, and then substring from that string? Commented Jun 5, 2011 at 0:28
  • You can't just ignore tags, otherwise you will end up with not valid html: ab<x>cd<em>ef</x>f</em>. You would need to do something like ab<x>cd</x><em><x>ef</x>g</em> Commented Jun 5, 2011 at 0:37
  • 1
    Of course I can't ignore the tags, but it would be nice if the browser could solve those problems for me in some way. Commented Jun 5, 2011 at 11:03

5 Answers 5

72

Below is a function to set the selection to a pair of character offsets within a particular element. This is naive implementation: it does not take into account any text that may be made invisible (either by CSS or by being inside a <script> or <style> element, for example) and may have browser discrepancies (IE versus everything else) with line breaks, and takes no account of collapsed whitespace (such as 2 or more consecutive space characters collapsing to one visible space on the page). However, it does work for your example in all major browsers.

For the other part, the highlighting, I'd suggest using document.execCommand() for that. You can use my function below to set the selection and then call document.execCommand(). You'll need to make the document temporarily editable in non-IE browsers for the command to work. See my answer here for code: getSelection & surroundContents across multiple tags

Here's a jsFiddle example showing the whole thing, working in all major browsers: http://jsfiddle.net/8mdX4/1211/

And the selection setting code:

function getTextNodesIn(node) {
    var textNodes = [];
    if (node.nodeType == 3) {
        textNodes.push(node);
    } else {
        var children = node.childNodes;
        for (var i = 0, len = children.length; i < len; ++i) {
            textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
        }
    }
    return textNodes;
}

function setSelectionRange(el, start, end) {
    if (document.createRange && window.getSelection) {
        var range = document.createRange();
        range.selectNodeContents(el);
        var textNodes = getTextNodesIn(el);
        var foundStart = false;
        var charCount = 0, endCharCount;

        for (var i = 0, textNode; textNode = textNodes[i++]; ) {
            endCharCount = charCount + textNode.length;
            if (!foundStart && start >= charCount
                    && (start < endCharCount ||
                    (start == endCharCount && i <= textNodes.length))) {
                range.setStart(textNode, start - charCount);
                foundStart = true;
            }
            if (foundStart && end <= endCharCount) {
                range.setEnd(textNode, end - charCount);
                break;
            }
            charCount = endCharCount;
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (document.selection && document.body.createTextRange) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(el);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    }
}
Sign up to request clarification or add additional context in comments.

10 Comments

Brilliant solution @tim-down! Your code was adapted to consolidating nested HTML formatted text. stackoverflow.com/questions/16226671/…
When I run this code a span tag is added to all the text which is highlighted , Can I add a class and a click event to that span tag ?
@prateek: I assume you mean the highlighting code? Since the spans are added automatically by the browser, it's not easy to find them and modify them. You could use the class applier module of my Rangy library instead.
@TimDown isn't this possible while creating the span tags I can add some attributes and event like you have added background color
@prateek: The spans that apply the background colour are generated by the browser in response to a simple call of document.execCommand(...). You could search for spans by checking the computed value of background-color for each span in the document, as in stackoverflow.com/questions/8076341/…
|
3

You could take a look at how works this powerful JavaScript utility which support selection over multiple DOM elements:

MASHA (short for Mark & Share) allow you to mark interesting parts of web page content and share it

http://mashajs.com/index_eng.html

It's also on GitHub https://github.com/SmartTeleMax/MaSha

Works even on Mobile Safari and IE!

Comments

3

I know that the question is not actually relevant, but this is what I was actually searching for.

If you need to Highlight SELECTED TEXT

Use the following principle: operate with Selection Range methods, like this


document.getSelection().getRangeAt(0).surroundContents(YOUR_WRAPPER_NODE) // Adds wrapper
document.getSelection().getRangeAt(0).insertNode(NEW_NODE) // Inserts a new node

That's it, I recommend you to study more about Range methods.

I was struggling with this and my searching requests were incorrect, so I decided to post it here for the case there will be people like me.

Sorry again for irrelevant answer.

2 Comments

I believe that in your code snippet YOUR_WRAPPER_NODE and NEW_NODE are the same entity, so it would be a good idea to name them the same. Also, your solution doesn't work when the range contains partially selected elements. In that case, replace the call to surroundContents with YOUR_WRAPPER_NODE.appendChild( document.getSelection().getRangeAt(0).extractContents()).
I just show methods that can be used, actually there is no connection between these lines, it's just example of methods. And yes it may not work for all cases, because again it's meant to be just an example. I just didn't want to make even more irelevant, breaking down the whole thing. Althought, thank you for sharing this knowledge ^_^
0

Following solution doesn't work for IE, you'll need to apply TextRange objects etc. for that. As this uses selections to perform this, it shouldn't break the HTML in normal cases, for example:

<div>abcd<span>efg</span>hij</div>

With highlight(3,6);

outputs:

<div>abc<em>d<span>ef</span></em><span>g</span>hij</div>

Take note how it wraps the first character outside of the span into an em, and then the rest within the span into a new one. Where as if it would just open it at character 3 and end at character 6, it would give invalid markup like:

<div>abc<em>d<span>ef</em>g</span>hij</div>

The code:

var r = document.createRange();
var s = window.getSelection()

r.selectNode($('div')[0]);
s.removeAllRanges();
s.addRange(r);

// not quite sure why firefox has problems with this
if ($.browser.webkit) {
    s.modify("move", "backward", "documentboundary");
}

function highlight(start,end){
    for(var st=0;st<start;st++){
        s.modify("move", "forward", "character");
    }

    for(var st=0;st<(end-start);st++){
        s.modify("extend", "forward", "character");
    }
}

highlight(2,6);

var ra = s.getRangeAt(0);
var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents()); 
ra.insertNode(newNode);

Example: http://jsfiddle.net/niklasvh/4NDb9/

edit Looks like at least my FF4 had some issues with

s.modify("move", "backward", "documentboundary");

but at the same time, it seems to work without it, so I just changed it to

if ($.browser.webkit) {
        s.modify("move", "backward", "documentboundary");
}

edit as Tim Pointed out, modify is only available from FF4 onwards, so I took a different approach to getting the selection, which doesn't need the modify method, in hopes in making it a bit more browser compatible (IE still needs its own solution).

The code:

var r = document.createRange();
var s = window.getSelection()

var pos = 0;

function dig(el){
    $(el).contents().each(function(i,e){
        if (e.nodeType==1){
            // not a textnode
         dig(e);   
        }else{
            if (pos<start){
               if (pos+e.length>=start){
                range.setStart(e, start-pos);
               }
            }

            if (pos<end){
               if (pos+e.length>=end){
                range.setEnd(e, end-pos);
               }
            }            

            pos = pos+e.length;
        }
    });  
}
var start,end, range;

function highlight(element,st,en){
    range = document.createRange();
    start = st;
    end = en;
    dig(element);
    s.addRange(range);

}
highlight($('div'),3,6);

var ra = s.getRangeAt(0);

var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents()); 
ra.insertNode(newNode);

example: http://jsfiddle.net/niklasvh/4NDb9/

4 Comments

Firefox only implements Selection.modify() since Firefox 4.0 and doesn't support all the granularity settings that WebKit does. Specifically, it doesn't support "documentboundary". See developer.mozilla.org/en/DOM/selection/modify
This seems to work only if there is nothing else on the page. See for example: jsfiddle.net/4NDb9/89. Although Hello world is outside the div, it is being highlighted, instead of the text inside the div.
@Tim Down Very good point. I rewrote a good portion of it to get rid of the modify() method now.
@Vincent Should be sorted in this rewritten code, have a look at jsfiddle.net/niklasvh/4NDb9/93. The highlight function now requires the first variable to be an element from which to look.
0

Based on the ideas of the jQuery.highlight plugin.

    private highlightRange(selector: JQuery, start: number, end: number): void {
        let cur = 0;
        let replacements: { node: Text; pos: number; len: number }[] = [];

        let dig = function (node: Node): void {
            if (node.nodeType === 3) {
                let nodeLen = (node as Text).data.length;
                let next = cur + nodeLen;
                if (next > start && cur < end) {
                    let pos = cur >= start ? cur : start;
                    let len = (next < end ? next : end) - pos;
                    if (len > 0) {
                        if (!(pos === cur && len === nodeLen && node.parentNode &&
                            node.parentNode.childNodes && node.parentNode.childNodes.length === 1 &&
                            (node.parentNode as Element).tagName === 'SPAN' && (node.parentNode as Element).className === 'highlight1')) {

                            replacements.push({
                                node: node as Text,
                                pos: pos - cur,
                                len: len,
                            });
                        }
                    }
                }
                cur = next;
            }
            else if (node.nodeType === 1) {
                let childNodes = node.childNodes;
                if (childNodes && childNodes.length) {
                    for (let i = 0; i < childNodes.length; i++) {
                        dig(childNodes[i]);
                        if (cur >= end) {
                            break;
                        }
                    }
                }
            }
        };

        selector.each(function (index, element): void {
            dig(element);
        });

        for (let i = 0; i < replacements.length; i++) {
            let replacement = replacements[i];
            let highlight = document.createElement('span');
            highlight.className = 'highlight1';
            let wordNode = replacement.node.splitText(replacement.pos);
            wordNode.splitText(replacement.len);
            let wordClone = wordNode.cloneNode(true);
            highlight.appendChild(wordClone);
            wordNode.parentNode.replaceChild(highlight, wordNode);
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.