views:

806

answers:

5

Hi,

I want to highlight a specific word in my HTML page after the page is loaded. I don't want to use the dumb:

document.innerHTML = document.innerHTML.replace(.....);

I want to traverse every DOM node, find out the ones that contain text and modify the innerHTML of only those individual nodes. Here's what I came up with:

function highlightSearchTerms(sword) {
$$('body').map(Element.extend).first().descendants().each(function (el) {
 if (el.nodeType == Node.ELEMENT_NODE && el.tagName != 'TD') {
  //$A(el.childNodes).each(function (onlyChild) {
   //if (onlyChild.nodeType == Node.TEXT_NODE) {
    //console.log(onlyChild);
    el.innerHTML = el.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
   //}
  //});
 }
});
//document.body.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
}

It works as it is right now, but it's VERY inefficient and is hardly better than the single line above as it may do a replacement several times over the same text. (Hmmm..., or not?)

If you uncomment the commented stuff and change el.innerHTML.replace to onlyChild.textContent.replace it would work almost like it needs to, but modifying textContent doesn't create a new span as an element, but rather adds the HTML content as text.

My question/request is to find a way that it highlights the words in the document traversing elements one by one.

A: 

Grab $(document.body) and do a search/replace and wrap a span around the term, then swap the entire $(document.body) in one go. Treat it as a big string, forget about the DOM. This way you only have to update the DOM once. It should be very quick.

Diodeus
I think this would cause problems if the word he wanted to highlight happened to be something like: "input" or "span" or "value, basically anything that could be in the markup as opposed to the displayed text
Mike Valstar
That's exactly the situation I've noticed now. No go.
A: 

I have found a script that will do what you want (it seems pretty fast), it is not specific to any library so you may want to modify it:

http://www.nsftools.com/misc/SearchAndHighlight.htm

The method you provided above (although commented out) will have problems with replacing items that might be inside a an html element. ie ` a search and replace might "highlight" "thing" when that would not be what you want.

here is a Jquery based highlight script:

http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html

It dosent look to hard to convert to prototype.

Mike Valstar
I know http://www.nsftools.com/misc/SearchAndHighlight.htm and used it until today, when I found out it's not working with Ajax somehow. And it's doing the same innerHTML processing - just a bit more fancy.
I know http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html as well and I've followed your suggestion even before you suggested it. I've got it working based on this jQuery code, but I still don't understand how it works. See my own answer below.
+1  A: 

This works quick and clean:

function highlightSearchTerms(sword) {
$$('body').map(Element.extend).first().descendants().each(function (el) {
 if (el.nodeType == Node.ELEMENT_NODE && el.tagName != 'TEXTAREA' && el.tagName != 'INPUT' && el.tagName != 'SCRIPT') {
  $A(el.childNodes).each(function (onlyChild) {
   var pos = onlyChild.textContent.indexOf(sword);
   if (onlyChild.nodeType == Node.TEXT_NODE && pos >= 0) {
    //console.log(onlyChild);
    var spannode = document.createElement('span');
       spannode.className = 'highlight';
       var middlebit = onlyChild.splitText(pos);
       var endbit = middlebit.splitText(sword.length);
       var middleclone = middlebit.cloneNode(true);
       spannode.appendChild(middleclone);
       middlebit.parentNode.replaceChild(spannode, middlebit);

    //onlyChild. = el.innerHTML.replace(new RegExp('('+sword+')', 'gi'), '<span class="highlight">$1</span>');
   }
  });
 }
});
}

But I've trouble understanding how exactly it works. This seems to be the magic line:

middlebit.parentNode.replaceChild(spannode, middlebit);
yay for prototype port of the jquery one i posted :) I'm going to go ahead and steal this for my own site.
Mike Valstar
A: 

I converted one from jQuery to PrototypeJS some time ago :

Element.addMethods({
  highlight: function(element, term, className) {
    function innerHighlight(element, term, className) {
      className = className || 'highlight';
      term = (term || '').toUpperCase();

      var skip = 0;
      if ($(element).nodeType == 3) {
        var pos = element.data.toUpperCase().indexOf(term);
        if (pos >= 0) {
          var middlebit = element.splitText(pos),
              endbit = middlebit.splitText(term.length),
              middleclone = middlebit.cloneNode(true),
              spannode = document.createElement('span');

          spannode.className = 'highlight';
          spannode.appendChild(middleclone);
          middlebit.parentNode.replaceChild(spannode, middlebit);
          skip = 1;
        }
      }
      else if (element.nodeType == 1 && element.childNodes && !/(script|style)/i.test(element.tagName)) {
        for (var i = 0; i < element.childNodes.length; ++i)
          i += innerHighlight(element.childNodes[i], term);
      }
      return skip;
    }
    innerHighlight(element, term, className);
    return element;
  },
  removeHighlight: function(element, term, className) {
    className = className || 'highlight';
    $(element).select("span."+className).each(function(e) {
      e.parentNode.replaceChild(e.firstChild, e);
    });
    return element;
  }
});

You can use it on every element like this:

$("someElementId").highlight("foo", "bar");

, and use the className of your choice. You can also remove the highlights.

Fabien Ménager
+1  A: 

if you're using the prototype version posted by Fabien, make sure to add the className as argument to the call of innerHighlight:

i += innerHighlight(element.childNodes[i], term)

needs to be

i += innerHighlight(element.childNodes[i], term, className)

if you care about custom classNames for your highlights.

geapi