Full-text search in JavaScript (part II)

Some years ago, I wrote a post about how to perform a full-text search in JavaScript modifying the DOM to highlight the results.

The aforementioned post has been outdated and there are better ways to perform a search in JavaScript, without using complex regular expressions, and without rewriting completely the HTML container on each search.

A better solution to perform a full-text search in JavaScript

It is possible to just do a recursive search in all the elements inside the desired HTML container, focusing only on Element and Text nodes. If the node is of type Element, the search is performed again inside it, if the node is of type Text, we check if it contains a match of the search, if it does, we replace it by an element with the highlighted results, otherwise, we just ignore it.

forEach utility

Before anything, let‘s create a small utility to iterate through Element’s childNodes. For this purpose we can use the Array.prototype.forEach method.

The childNodes property of an Element returns a NodeList and this object contains a forEach method. But this method is not supported on Internet Explorer. If you don‘t need to cover this browser you can ignore this section and use the native method instead.

function forEach(node, callback) {                    
    Array.prototype.forEach.call(node.childNodes, callback);
}

In this way, we just need to do the next to iterate through the childNodes of an Element:

forEach(node, function (child) {
    // Do something with each child
});

Recursive function to perform a full-text search in JavaScript

Now, let‘s create a function to recursively search Text nodes in a container. If they contain a match inside, we just need to replace it with a span element that contains the original text with the matches, each one wrapped also in a span with a specific class.

/*
** Build a case insensitive regular expression that
** searches globaly for a specific text
*/ 
var reg = new RegExp("(" + search + ")", "gi");

function highlightSearchInNode (parentNode, search) {

    forEach(parentNode, function (node) {

        // If the node is a container, repeat the search inside it
        if (node.nodeType === 1) {

            highlightSearchInNode(node, search);

        // If the node is of type Text, check if it contains any match
        } else if (
            node.nodeType === 3 &&
            reg.test(node.nodeValue)
        ) {

            // Create a span element
            var span = document.createElement("span");
            // Add a data attribute to the span to indentify it later
            span.dataset.search = "true";
            // Insert the same text inside the span, replacing the matches with spans with a specific class
            span.innerHTML = node.nodeValue.replace(reg, "<span class='found'>$1</span>");
            // Replace the node with the created span
            parentNode.replaceChild(span, node);

        }
    });
}

Using the previous function, if we search the word “can” in the next HTML code, the next result is returned:

<p>
    Can you can the can that I can can?
</p>
<p>
    <span data-search="true"><span class="found">Can</span> you <span class="found">can</span> the <span class="found">can</span> that I <span class="found">can</span> <span class="found">can</span>?
</p>

Recursive cleaning function

Now we need a function to reset the container to its initial state (before performing a new search we need to start over a clean container).

function cleanAllSearchSpans (parentNode) {

    forEach(parentNode, function (node) {

        // If the node is of type Element
        if (node.nodeType === 1) {
            // If the node is of type span and it has the data-search property
            if (
                node.nodeName === "SPAN" &&
                node.dataset.search === "true"
            ) {
                // Replace the node with a Text node with the span inner text
                parentNode.replaceChild(
                    document.createTextNode(node.innerText),
                    node
                );
            // Otherwise, repeat the search within the element
            } else {
                cleanAllSearchSpans(node);
            }
        }
    });
};

Final code for the full-text search in JavaScript

With these two functions, it is possible to create a function that primarily cleans any previous search, then performs a new search and returns the number of matches (using the same regular expression):

function forEach(node, callback) {
    Array.prototype.forEach.call(node.childNodes, callback);
}

function searchText(container, search) {

    var total = 0;
    var reg = new RegExp("(" + search + ")", "gi");

    var cleanAllSearchSpans = function (parentNode) {
        forEach(parentNode, function (node) {
            if (node.nodeType === 1) {
                if (
                    node.nodeName === "SPAN" &&
                    node.dataset.search === "true"
                ) {
                    parentNode.replaceChild(
                        document.createTextNode(node.innerText),
                        node
                    );
                } else {
                    cleanAllSearchSpans(node);
                }
            }
        });
    };

    var highlightSearchInNode = function (parentNode, search) {
        forEach(parentNode, function (node) {
            if (node.nodeType === 1) {
                highlightSearchInNode(node, search);
            } else if (
                node.nodeType === 3 &&
                reg.test(node.nodeValue)
            ) {
                var matches = node.nodeValue.match(reg);
                var span = document.createElement("span");
                span.dataset.search = "true";
                span.innerHTML = node.nodeValue.replace(reg, "$1");
                parentNode.replaceChild(span, node);
                total += matches.length;
            }
        });
    };

    cleanAllSearchSpans(container);
    // Normalise the container to remove the text siblings nodes
    container.normalize();
    highlightSearchInNode(container, search);

    return total;
}

Full-text search in JavaScript (part II) demo

Demo

Download the demo files

Download the demo files

(4 votes, average: 4.00 Out Of 5)
Share this post:

It is possible to insert code fragments between <pre></pre> tags and HTML or XML code between <xmp></xmp> tags.

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.