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
Download the demo files
Download the demo files
It is possible to insert code fragments between <pre></pre> tags and HTML or XML code between <xmp></xmp> tags.