views:

68

answers:

4

I would like to highlight search terms on a page, but not mess with any HTML tags. I was thinking of something like:

$('.searchResult *').each(function() {
    $(this.html($(this).html().replace(new RegExp('(term)', 'gi'), '<span class="highlight">$1</span>'));
)};

However, $('.searchResult *').each matches all elements, not just leaf nodes. In other words, some of the elements matched have HTML inside them. So I have a few questions:

  1. How can I match only leaf nodes?
  2. Is there some built-in jQuery RegEx function to simplify things? Something like: $(this).wrap('term', $('<span />', { 'class': 'highlight' }))
  3. Is there a way to do a simple string replace and not a RegEx?
  4. Any other better/faster way of doing this?

Thanks so much!

+1  A: 

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.

Anurag
dis is buggy, we can't set the `nodeValue` of a text node and hope it will work :). have to replace the text node with a span element.
Anurag
Fixed the bugs, now only does text node replacements. Does **not** replace the entire `html`.
Anurag
it will fail for things like `(this)`
galambalazs
@galambalazs - could you elaborate more on why `(this)` would be a breaking input?
Anurag
`(this)` will highlight `this`, not `(this)` http://jsfiddle.net/Bf8Rs/2/
galambalazs
and all valid regexes like: `.*`, `question?`, `10*10`, ....
galambalazs
ah I see, thanks for pointing that out. I am tempted to use your `RegExp.escape` solution, but will let this bug pass instead :)
Anurag
+3  A: 

[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 || el.textContent || el.innerText;
      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);
  });
}
galambalazs
The `See it in action` example isn't working for me. However, I had forgotten about the `:contains` selector which should help with selecting the "leaf" nodes and not doing a replace unnecessarily. I'll give this a try.
Nelson
I'm guessing it would be more efficient to create the RegExp variable once before the `each` and reuse it inside `each`?
Nelson
Yes it will be precompiled and should be faster. **Check** the link now :)
galambalazs
@galambalazs @Nelson - `contains` would do a case insensitive search, so it might be better to do a regex search on `text()` instead. Also, any solution where `html` is being overwritten suffers from two problems - existing behavior such as events will get overwritten, and the search term may collide with the html. See http://jsfiddle.net/BcsQG/1/
Anurag
I've updated the answer.
galambalazs
A: 

Here's a naive implementation that just blasts in HTML for any match:

<!DOCTYPE html>
<html lang"en">
<head>
    <title>Select Me</title>
    <style>
        .highlight {
            background:#FF0;
        }
    </style>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js"&gt;&lt;/script&gt;
    <script type="text/javascript">

        $(function () {

            hightlightKeyword('adipisicing');

        });

        function hightlightKeyword(keyword) {

            var replacement = '<span class="highlight">' + keyword + '</span>';
            var search = new RegExp(keyword, "gi");
            var newHtml = $('body').html().replace(search, replacement);
            $('body').html(newHtml);
        }

    </script>
</head>
<body>
    <div>

        <p>Lorem ipsum dolor sit amet, consectetur <b>adipisicing</b> elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
        <p>Lorem ipsum dolor sit amet, <em>consectetur adipisicing elit</em>, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>

    </div>
</body>
</html>
AndrewDotHay
Yeah, the problem with that is it can match on html tags. If the keyword is `p` (paragraph), your HTML is mangled.
Nelson
+1  A: 

I'd give the Highlight jQuery plugin a shot.

Chris Doggett
I saw that, but it does a temporary highlight. I need to keep the terms highlighted. Also, the fade effect probably wouldn't be a good idea with dozens or hundreds of matches on a page.
Nelson
You may have clicked it before I edited it. I had the wrong link originally. The one in jQueryUI is indeed temporary, but the one on johannburkard.de is permanent until you call removeHighlight(), and doesn't have a fade effect.
Chris Doggett
The new link works as expected. I may end up using this in the end, but galambalazs answered my questions more directly.
Nelson