Highlight search terms (select only leaf nodes)

Solution 1:

[See it in action]

// escape by Colin Snover
// Note: if you don't care for (), you can remove it..
RegExp.escape = function(text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

function highlight(term, base) {
  if (!term) return;
  base = base || document.body;
  var re = new RegExp("(" + RegExp.escape(term) + ")", "gi"); //... just use term
  var replacement = "<span class='highlight'>" + term + "</span>";
  $("*", base).contents().each( function(i, el) {
    if (el.nodeType === 3) {
      var data = el.data;
      if (data = data.replace(re, replacement)) {
        var wrapper = $("<span>").html(data);
        $(el).before(wrapper.contents()).remove();
      }
    }
  });
}

function dehighlight(term, base) {
  var text = document.createTextNode(term);
  $('span.highlight', base).each(function () {
    this.parentNode.replaceChild(text.cloneNode(false), this);
  });
}

Solution 2:

Use contents()1, 2, 3 to get all nodes including text nodes, filter out the non-text nodes, and finally replace the nodeValue of each remaining text node using regex. This would keep the html nodes intact, and only modify the text nodes. You have to use regex instead of simple string substitutions as unfortunately we cannot do global replacements when the search term is a string.

function highlight(term) {
    var regex = new RegExp("(" + term + ")", "gi");
    var localRegex = new RegExp("(" + term + ")", "i");
    var replace = '<span class="highlight">$1</span>';

    $('body *').contents().each(function() {
        // skip all non-text nodes, and text nodes that don't contain term
        if(this.nodeType != 3 || !localRegex.test(this.nodeValue)) {
            return;
        }
        // replace text node with new node(s)
        var wrapped = $('<div>').append(this.nodeValue.replace(regex, replace));
        $(this).before(wrapped.contents()).remove();
    });
}

We can't make it a one-liner and much shorter easily now, so I prefer it like this :)

See example here.

Solution 3:

I'd give the Highlight jQuery plugin a shot.

Solution 4:

I've made a pure JavaScript version of this, and packaged it into a Google Chrome plug-in, which I wish to be helpful to some people. The core function is shown below:

GitHub Page for In-page Highlighter

function highlight(term){
    if(!term){
        return false;
    }

    //use treeWalker to find all text nodes that match selection
    //supported by Chrome(1.0+)
    //see more at https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
    var treeWalker = document.createTreeWalker(
        document.body,
        NodeFilter.SHOW_TEXT,
        null,
        false
        );
    var node = null;
    var matches = [];
    while(node = treeWalker.nextNode()){
        if(node.nodeType === 3 && node.data.indexOf(term) !== -1){
            matches.push(node);
        }
    }

    //deal with those matched text nodes
    for(var i=0; i<matches.length; i++){
        node = matches[i];
        //empty the parent node
        var parent = node.parentNode;
        if(!parent){
            parent = node;
            parent.nodeValue = '';
        }
        //prevent duplicate highlighting
        else if(parent.className == "highlight"){
            continue;
        }
        else{
            while(parent && parent.firstChild){
                parent.removeChild(parent.firstChild);
            }
        }

        //find every occurance using split function
        var parts = node.data.split(new RegExp('('+term+')'));
        for(var j=0; j<parts.length; j++){
            var part = parts[j];
            //continue if it's empty
            if(!part){
                continue;
            }
            //create new element node to wrap selection
            else if(part == term){
                var newNode = document.createElement("span");
                newNode.className = "highlight";
                newNode.innerText = part;
                parent.appendChild(newNode);
            }
            //create new text node to place remaining text
            else{
                var newTextNode = document.createTextNode(part);
                parent.appendChild(newTextNode);
            }
        }

    }
}