30

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

coulbourne
  • 489
  • 1
  • 8
  • 16
  • 1
    Go ahead and [add jQuery with your userscript](http://stackoverflow.com/a/12751531/331508). Don't waste time trying to recreate it in pieces. – Brock Adams Jul 23 '13 at 00:10
  • If you're prepared to drop support for some browsers (I think just IE7 - EDIT: Also maybe IE8), you can use the native "document.querySelectorAll" in a very similar way to JQuery's selector. http://caniuse.com/#feat=queryselector – Katana314 Jul 23 '13 at 01:14

6 Answers6

38

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
...
Dorian
  • 1,079
  • 11
  • 19
elclanrs
  • 92,861
  • 21
  • 134
  • 171
  • 4
    Possibly use `Array.prototype.filter.call` instead of allocating a new array. – a paid nerd Jun 05 '14 at 03:03
  • This is incorrect because it also includes results for all child nodes. I.e. if child node of `element` will contain `text` - `element` will be included into `contains` result; which is wrong. – avalanche1 Oct 13 '19 at 14:57
10

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("❤")))
br.
  • 1,259
  • 1
  • 14
  • 21
6

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

Claudio Redi
  • 67,454
  • 15
  • 130
  • 155
5

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')));

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
3

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())
    );
  }
avalanche1
  • 3,154
  • 1
  • 31
  • 38
2

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.

Elad Meidar
  • 774
  • 6
  • 11