Hace varios años atrás escribí un post acerca de cómo realizar una búsqueda de texto en JavaScript, modificando el DOM para resaltar los resultados.
Dicho post ha quedado desfasado y hay mejores maneras de hacer una búsqueda de texto en JavaScript, sin tener para ello que hacer uso de complejas expresiones regulares, ni tener que reescribir todo el HTML del contenedor para lograr este objetivo.
Mejor solución para hacer una búsqueda de texto en JavaScript
Podríamos simplemente hacer una búsqueda recursiva de todos los elementos dentro del contenedor deseado, centrándonos solo en nodos de tipo Element y los nodos de tipo Text. Si el nodo es de tipo Element, repetimos la búsqueda dentro del mismo, si el nodo es de tipo Text chequeamos si contiene algún resultado de la búsqueda, si es así lo remplazamos por un elemento con los resultados resaltados y si no contiene resultados simplemente lo ignoramos.
Utilidad forEach
Antes que nada, vamos a crear una pequeña utilidad para iterar en los childNodes de un nodo de tipo Element, para ello nos valdremos del método Array.prototype.forEach:
function forEach(node, callback) {
Array.prototype.forEach.call(node.childNodes, callback);
}
De esta manera solo tendremos que hacer lo siguiente para iterar en los nodos hijos de un elemento:
forEach(node, function (hijo) {
// Hacer algo con cada uno de los hijos
});
Función recursiva de búsqueda de texto en JavaScript
Después crearemos una función que recursivamente busque elementos de tipo Text y si estos contienen algún resultado dentro, entonces remplace el elemento por un span que contenga el texto original con los resultados envueltos cada uno por un span con una clase específica.
/*
** Crear una expresión regular que busque sin distinción de mayúsculas y minúsculas
** y de manera global un grupo con un texto específico
*/
var reg = new RegExp("(" + search + ")", "gi");
function highlightSearchInNode (parentNode, search) {
forEach(parentNode, function (node) {
// Si el nodo es un contenedor, repetir la búsqueda dentro del mismo
if (node.nodeType === 1) {
highlightSearchInNode(node, search);
// Si el nodo es de tipo texto, analizar si contiene un resultado
} else if (
node.nodeType === 3 &&
reg.test(node.nodeValue)
) {
// Crear un elemento de tipo span
var span = document.createElement("span");
// Añadir un atributo de tipo data al span para identificarlo más tarde
span.dataset.search = "true";
// Dentro del span añadir el mismo texto remplazando las coincidencias con un span con una clase específica
span.innerHTML = node.nodeValue.replace(reg, "<span class='found'>$1</span>");
// Remplazar el nodo por el span creado
parentNode.replaceChild(span, node);
}
});
}
Con la anterior función, si buscamos la palabra «cuesta» en el siguiente código HTML, obtendremos el resultado a continuación:
<p>
A Cuesta le cuesta subir la cuesta, y en medio de la cuesta, va y se acuesta.
</p>
<p>
<span data-search="true">A <span class="found">Cuesta</span> le <span class="found">cuesta</span> subir la <span class="found">cuesta</span>, y en medio de la <span class="found">cuesta</span>, va y se a<span class="found">cuesta</span>.</span>
</p>
Función recursiva de limpieza
Ya solo nos quedaría tener una función para deshacer las búsquedas y dejar todo en el estado inicial.
function cleanAllSearchSpans (parentNode) {
forEach(parentNode, function (node) {
// Si el nodo es de tipo Element
if (node.nodeType === 1) {
// Si el nodo es de tipo span y tiene la propiedad data-search
if (
node.nodeName === "SPAN" &&
node.dataset.search === "true"
) {
// Remplazar el nodo por un nodo de tipo texto con el texto dentro del span
parentNode.replaceChild(
document.createTextNode(node.innerText),
node
);
// Si no, repetir la búsqueda dentro del Element
} else {
cleanAllSearchSpans(node);
}
}
});
};
Código final para búsqueda de texto en JavaScript
Con estas dos funciones ya podríamos crear una función que primeramente se encargara de limpiar cualquier búsqueda anterior, después realizara una nueva búsqueda y nos retornara la cantidad de veces que se encontró (usando la misma expresión regular):
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, "<span class='found'>$1</span>");
parentNode.replaceChild(span, node);
total += matches.length;
}
});
};
cleanAllSearchSpans(container);
// Normalizar el contenedor para eliminar los nodos hermanos de tipo Text
container.normalize();
highlightSearchInNode(container, search);
return total;
}
Demostración de la función de búsqueda de texto en JavaScript (parte II)
Descarga de ficheros
Descarga los ficheros de ejemplo
Excelente Script. Estoy buscando algo parecido, pero cuando el texto es muy largo sería necesario incluir el poder navegar entre los resultados, con enlaces de «Anterior» y «Siguiente»
Hola Anton, tendrías que modificar el código para hacer lo que estás buscando. En este caso los resultados se buscan recursivamente y se les aplica el cambio a todos.
Para hacer lo que deseas, primeramente guardaría en un array la referencia de cada elemento a cambiar ya envuelto en un `span` pero sin la clase aplicada, y después, en la función de anterior y siguiente, solo le cambiaría la clase a cada uno de los `span` para señalar el resultado.