6

I am trying to create a text search function but I am having a hard time getting it to work when there is html inside the element. Here is some simple html to demonstrate my problem.

<b>
    <input type="checkbox" value="" />
    I need replaced
</b>

And here is where I am currently at for javascript. It works great assuming there is no html inside.

$("*", search_container).each(function() {
    var replaceTxt = $(this).text().replace(new RegExp("(" + search_term + ")", 'i'), '<span style="color: #0095DA;" class="textSearchFound">$1</span>');
    $(this).text().replaceWith(replaceTxt);
}); 

As the user is typing I need to replace the text with the span. So the content should look like the following as he/she types.

<b>
    <input type="checkbox" value="" />
    <span style="color: #0095DA" class="textSearchFound">I need</span> replaced
</b>

UPDATE

After looking over the question that was voted a duplicate I came up with this. Which although may be close, it inserts the html into the DOM as text which I do not want. Please vote to reopen this.

$("*", search_container).each(function() {
    var node = $(this).get(0);
    var childs = node.childNodes;
    for(var inc = 0; inc < childs.length; inc++) {
        //Text node
        if(childs[inc].nodeType == 3){ 
            if(childs[inc].textContent) {
                childs[inc].textContent = childs[inc].textContent.replace(new RegExp("(" + search_term + ")", 'i'), '<span style="color: #0095DA;" class="textSearchFound">$1</span>');
             } 
             //IE
             else {
                 childs[inc].nodeValue = childs[inc].nodeValue.replace(new RegExp("(" + search_term + ")", 'i'), '<span style="color: #0095DA;" class="textSearchFound">$1</span>');
            }
        }
    }
});
Metropolis
  • 6,542
  • 19
  • 56
  • 86
  • Is this to be used on a page you can control the html for, or a more general case where you want to be able to replace text and you don't have control of the structure of the html? – Jason Aller Mar 10 '14 at 16:07
  • @JasonAller A more general cause. I need this to work under all conditions where I dont know what will be in the elements. – Metropolis Mar 10 '14 at 16:11
  • Maybe try this plugin? http://bartaz.github.io/sandbox.js/jquery.highlight.html – wirey00 Mar 10 '14 at 16:15
  • That other one that is supposed to be a duplicate does not work while trying to insert html. – Metropolis Mar 10 '14 at 17:06
  • 5
    This question has been erroneously marked as a duplicate. The question which is referenced as the dupe does not cover the use case specified in this question. It only handles cases where the user wants to replace text with text, not replacing a section of text with new HTML elements, so the solution is significantly different. – nderscore Mar 21 '14 at 04:09

2 Answers2

6

It's not pretty, but the best way to do this would be to loop through every node on the page recursively. When you come across a text node, check if it contains your search string. If you find it, delete the original text node and replace it with a text node of the text before the match, a new element with the highlighted match, and a text node of what's after the match.

Here's an example function:

var highlightSomeText = function(needle, node){
    node = node || document.body; // initialize the first node, start at the body

    if(node.nodeName == 'SCRIPT') return; // don't mess with script tags 

    if(node.nodeType == 3){ // if node type is text, search for our needle
        var parts = node.nodeValue.split(needle); // split text by our needle
        if(parts.length > 1){ // if we found our needle
            var parent = node.parentNode
            for(var i = 0, l = parts.length; i < l;){
                var newText = document.createTextNode(parts[i++]); // create a new text node, increment i
                parent.insertBefore(newText, node);
                if(i != l){ // if we're not on the last part, insert a new span element for our highlighted text
                    var newSpan = document.createElement('span');
                    newSpan.className = 'textSearchFound';
                    newSpan.style.color = '#0095DA';
                    newSpan.innerHTML = needle;
                    parent.insertBefore(newSpan, node);
                }
            }
            parent.removeChild(node); // delete the original text node
        }
    }

    for(var i = node.childNodes.length; i--;) // loop through all child nodes
        highlightSomeText(needle, node.childNodes[i]);
};

highlightSomeText('I need');

And a demo on jsfiddle: http://jsfiddle.net/8Mpqe/


Edit: Here is an updated method which uses Regexp to be case-insensitive: http://jsfiddle.net/RPuRG/

Updated lines:

var parts = node.nodeValue.split(new RegExp('(' + needle + ')','i'));

Using a regular expression which is wrapped in a capturing group () the results of the capture will be spliced into the array of parts.

newSpan.innerHTML = parts[i++];

The text for the new span element will be the next part in our array.

Edit 2: Since this function is being called on every keyup event of some text field, the asker wanted to remove the highlighted span tags before each run. To do this, it was necessary to unwrap each highlighted element, then to call normalize on the parent node to merge adjacent text nodes back together. Here's a jQuery example:

$('.textSearchFound').each(function(i, el){
    $(el).contents().unwrap().
    parent()[0].normalize();
});

And here's the completed working demo: http://jsfiddle.net/xrL3F/

nderscore
  • 4,182
  • 22
  • 29
  • I am currently trying to fit this example into what I have lol. It looks like you got it working well, I just am trying to alter it and get it working. – Metropolis Mar 10 '14 at 19:03
  • I just updated it again to more closely match your original example. The new version should be case insensitive. If you only want to match text within some parent element, you can replace `document.body` in the first line with whatever root element you choose. – nderscore Mar 10 '14 at 19:07
  • Okay its close. I am using a keydown event for this function though, so I need to just loop through in the middle without the recursion and I need to unwrap the spans at the beginning of the function. – Metropolis Mar 10 '14 at 19:27
  • That is breaking up the text nodes so they can not be searched on again. It needs to be easily put in, and taken out. – Metropolis Mar 10 '14 at 19:45
  • This should reverse it: `$('.textSearchFound').each(function(i,el){ var $el = $(el); $el.replaceWith($el.text()); });` – nderscore Mar 10 '14 at 19:49
  • Ah, now I can see how this could be a problem if you're doing searches repeatedly onkeyup. You might be better off cloning the html elements first, then applying highlighting. – nderscore Mar 10 '14 at 19:58
  • @Metropolis you removed the recursion from the function, so it will only match elements which are direct children of your container element. Any sub-children would not be found. – nderscore Mar 10 '14 at 20:03
  • Sorry i didnt update it. This is the one I meant. http://jsfiddle.net/Metropolis/pq54k/1/ – Metropolis Mar 10 '14 at 20:04
  • Perfect! Thanks man! You may want to update your code and fiddle in your answer in case people are looking around for it. – Metropolis Mar 10 '14 at 20:39
  • 1
    One more thing to note, you should probably sanitize the input of the text field to escape any characters which are found in a regular expression, since the solution builds a regular expression from the entered string. Something like: `this.value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");` – nderscore Mar 10 '14 at 21:04
1

You can use the JQuery .contents() method to get the child nodes of an element. The text nodes will have nodeType == 3. You can then get the text of a text node using its textContent property.

var getNonEmptyChildTextNodes = function($element) {
    return $element.contents().filter(function() {
        return (this.nodeType == 3) && (this.textContent.trim() != '');
    });
};

var $textNode = getNonEmptyChildTextNodes($('b')).first(),
    text = $textNode[0].textContent;

Then to perform the replacement, you can do the following:

var search_term = 'I need',
    regExp = new RegExp(search_term, 'gi');

var result = regExp.exec(text);
if (result !== null) {
    var $elements = $(),
        startIndex = 0;
    while (result !== null) {
        $elements = $elements.add(document.createTextNode(text.slice(startIndex, result.index)));
        $elements = $elements.add('<span style="color: #0095DA;" class="textSearchFound">' + result[0] + '</span>');
        startIndex = regExp.lastIndex;
        result = regExp.exec(text);
    }
    $elements = $elements.add(document.createTextNode(text.slice(startIndex)));
    $textNode.replaceWith($elements);
}

jsfiddle


Here is a jsfiddle showing the complete search & wrap using the code above.

John S
  • 21,212
  • 8
  • 46
  • 56
  • He doesn't just want to replace text though, he wants to insert a new element where the matching text is. – nderscore Mar 10 '14 at 16:31
  • But what if I need it twice? http://jsfiddle.net/NarJ9/3/ And you are adding an additional span tag around everything. – nderscore Mar 10 '14 at 17:56
  • It's adding additional markup which could change how it is displayed. I think it is more important to be correct than easy. The regexp could also be simplified by removing the capturing group `()` and replacing `$1` with `$&` – nderscore Mar 10 '14 at 18:58