coulbourne
coulbourne

Reputation: 529

Native javascript equivalent of jQuery :contains() selector

I am writing a UserScript that will remove elements from a page that contain a certain string.

If I understand jQuery's contains() function correctly, it seems like the correct tool for the job.

Unfortunately, since the page I'll be running the UserScript on does not use jQuery, I can't use :contains(). Any of you lovely people know what the native way to do this is?

http://codepen.io/coulbourne/pen/olerh

Upvotes: 34

Views: 23912

Answers (6)

The original question is from 2013

Here is an even older solution, and the fastest solution because the main workload is done by the Browser Engine NOT the JavaScript Engine

The TreeWalker API has been around for ages, IE9 was the last browser to implement it... in 2011

All those 'modern' and 'super-modern' querySelectorAll("*") need to process all nodes and do string comparisons on every node.

The TreeWalker API gives you only the #text Nodes, and then you do what you want with them.

You could also use the NodeIterator API, but TreeWalker is faster

  function textNodesContaining(txt, root = document.body) {
      let nodes = [],
          node, 
          tree = document.createTreeWalker(
                            root, 
                               4, // NodeFilter.SHOW_TEXT
                               {
                                 node: node => RegExp(txt).test(node.data)
                               });
      while (node = tree.nextNode()) { // only return accepted nodes
        nodes.push(node);
      }
      return nodes;
  }

Usage

textNodesContaining(/Overflow/);

textNodesContaining("Overflow").map(x=>console.log(x.parentNode.nodeName,x));

// get "Overflow" IN A parent
textNodesContaining("Overflow")
   .filter(x=>x.parentNode.nodeName == 'A')
   .map(x=>console.log(x));

// get "Overflow" IN A ancestor
textNodesContaining("Overflow")
   .filter(x=>x.parentNode.closest('A'))
   .map(x=>console.log(x.parentNode.closest('A')));

Upvotes: 5

br.
br.

Reputation: 1299

Super modern one-line approach with optional chaining operator

[...document.querySelectorAll('*')].filter(element => element.childNodes?.[0]?.nodeValue?.match('❤'));

And better way is to search in all child nodes

[...document.querySelectorAll("*")].filter(e => e.childNodes && [...e.childNodes].find(n => n.nodeValue?.match("❤")))

Upvotes: 12

avalanche1
avalanche1

Reputation: 3592

This is the modern approach

function get_nodes_containing_text(selector, text) {
    const elements = [...document.querySelectorAll(selector)];

    return elements.filter(
      (element) =>
        element.childNodes[0]
        && element.childNodes[0].nodeValue
        && RegExp(text, "u").test(element.childNodes[0].nodeValue.trim())
    );
  }

Upvotes: 3

elclanrs
elclanrs

Reputation: 94121

This should do in modern browsers:

function contains(selector, text) {
  var elements = document.querySelectorAll(selector);
  return [].filter.call(elements, function(element){
    return RegExp(text).test(element.textContent);
  });
}

Then use it like so:

contains('p', 'world'); // find "p" that contain "world"
contains('p', /^world/); // find "p" that start with "world"
contains('p', /world$/i); // find "p" that end with "world", case-insensitive
...

Upvotes: 41

Claudio Redi
Claudio Redi

Reputation: 68440

If you want to implement contains method exaclty as jQuery does, this is what you need to have

function contains(elem, text) {
    return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1;
}

function getText(elem) {
    var node,
        ret = "",
        i = 0,
        nodeType = elem.nodeType;

    if ( !nodeType ) {
        // If no nodeType, this is expected to be an array
        for ( ; (node = elem[i]); i++ ) {
            // Do not traverse comment nodes
            ret += getText( node );
        }
    } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
        // Use textContent for elements
        // innerText usage removed for consistency of new lines (see #11153)
        if ( typeof elem.textContent === "string" ) {
            return elem.textContent;
        } else {
            // Traverse its children
            for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
                ret += getText( elem );
            }
        }
    } else if ( nodeType === 3 || nodeType === 4 ) {
        return elem.nodeValue;
    }
    // Do not include comment or processing instruction nodes

    return ret;
};

SOURCE: Sizzle.js

Upvotes: 6

Elad Meidar
Elad Meidar

Reputation: 814

Well, jQuery comes equipped with a DOM traversing engine that operates a lot better than the one i'm about to show you, but it will do the trick.

var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
  if (items[i].innerHTML.indexOf("word") != -1) { 
    // Do your magic
  }
}

Wrap it in a function if you will, but i would strongly recommend to use jQuery's implementation.

Upvotes: 3

Related Questions