Test if a selector matches a given element

Is there any way to test if a selector would match a given DOM Element? Preferably, without the use of an external library like Sizzle. This is for a library and I would like to minimize the amount of third party plugins required for the "core" library. If it ends up requiring Sizzle I'll just add that as a plugin to the library for those who want the feature it would enable.

For example, I would be able to do something like:

var element = <input name="el" />

matches("input[name=el]", element) == true

EDIT: After thinking about it more, I came up with a solution, this technically works, but it doesn't seem optimal in terms of efficiency:

function matchesSelector(selector, element) { 
    var nodeList = document.querySelectorAll(selector); 
    for ( var e in nodeList ) {
        return nodeList[e] === element; 
    }
    return false; 
}

Basically the function queries the entire document with the given selector, and then it iterates over the nodeList. If the given element is in the nodeList, then it returns true, and if it isn't it will return false.

If anyone can come up with a more efficient answer I would gladly mark their response as the answer.

EDIT: Flavius Stef pointed me towards a browser specific solution for Firefox 3.6+, mozMatchesSelector. I also found the equivalent for Chrome (version compatibility unknown, and it may or may not work on Safari or other webkit browsers): webkitMatchesSelector, which is basically the same as the Firefox implementation. I have not found any native implementation for the IE browsers yet.

For the above example, the usage would be:

element.(moz|webkit)MatchesSelector("input[name=el]")

It seems the W3C has also addressed this in the Selectors API Level 2 (still a draft at this moment) specification. matchesSelector will be a method on DOM Elements once approved.

W3C Usage: element.matchesSelector(selector)

Since that specification is still a draft and there is a lag time before popular browsers implement the methods once it becomes the standard, it may be a while until this actually usable. Good news is, if you use any of the popular frameworks, chances are they probably implement this functionality for you without having to worry about cross browser compatibility. Although that doesn't help those of us who can't include third party libraries.

Frameworks or libraries that implement this functionality:

http://www.prototypejs.org/api/element/match

http://developer.yahoo.com/yui/docs/YAHOO.util.Selector.html

http://docs.jquery.com/Traversing/is

http://extjs.com/deploy/dev/docs/output/Ext.DomQuery.html#Ext.DomQuery-methods

http://base2.googlecode.com/svn/doc/base2.html#/doc/!base2.DOM.Element.matchesSelector

http://wiki.github.com/jeresig/sizzle/


Solution 1:

For the benefit of those visiting this page after lo these many years, this functionality is now implemented in all modern browsers as element.matches without vendor prefix (except for ms for MS browsers other than Edge 15, and webkit for Android/KitKat). See http://caniuse.com/matchesselector.

Solution 2:

For best performance, use the browser implementations ((moz|webkit|o|ms)matchesSelector) where possible. When you can't do that, here is a manual implementation.

An important case to consider is testing selectors for elements not attached to the document.

Here's an approach that handles this situation. If it turns out the the element in question is not attached to the document, crawl up the tree to find the highest ancestor (the last non-null parentNode) and drop that into a DocumentFragment. Then from that DocumentFragment call querySelectorAll and see if the your element is in the resulting NodeList.

Here is the code.

The document

Here's a document structure we'll be working with. We'll grab the .element and test whether it matches the selectors li and .container *.

<!DOCTYPE html>
<html>
  <body>
    <article class="container">
      <section>
        <h1>Header 1</h1>
        <ul>
          <li>one</li>
          <li>two</li>
          <li>three</li>
        </ul>
      </section>
      <section>
        <h1>Header 2</h1>
        <ul>
          <li>one</li>
          <li>two</li>
          <li class="element">three</li>
        </ul>
      </section>
      <footer>Footer</footer>
    </article>
  </body>
</html>

Searching with document.querySelectorAll

Here is a matchesSelector function that uses document.querySelectorAll.

// uses document.querySelectorAll
function matchesSelector(selector, element) {
  var all = document.querySelectorAll(selector);
  for (var i = 0; i < all.length; i++) {
    if (all[i] === element) {
      return true;
    }
  }
  return false;
}

This works as long as that element is in the document.

// this works because the element is in the document
console.log("Part 1");
var element = document.querySelector(".element");
console.log(matchesSelector("li", element)); // true
console.log(matchesSelector(".container *", element)); // true

However, it fails if the element is removed from the document.

// but they don't work if we remove the article from the document
console.log("Part 2");
var article = document.querySelector("article");
article.parentNode.removeChild(article);
console.log(matchesSelector("li", element)); // false
console.log(matchesSelector(".container *", element)); // false

Searching within a DocumentFragment

The fix requires searching whatever subtree that element happens to be in. Here's an updated function named matchesSelector2.

// uses a DocumentFragment if element is not attached to the document
function matchesSelector2(selector, element) {
  if (document.contains(element)) {
    return matchesSelector(selector, element);
  }
  var node = element;
  var root = document.createDocumentFragment();
  while (node.parentNode) {
    node = node.parentNode;
  }
  root.appendChild(node);
  var all = root.querySelectorAll(selector);
  for (var i = 0; i < all.length; i++) {
    if (all[i] === element) {
      root.removeChild(node);
      return true;
    }
  }
  root.removeChild(node);
  return false;
}

Now we see that matchesSelector2 works even though the element is in a subtree that is detached from the document.

// but they will work if we use matchesSelector2
console.log("Part 3");
console.log(matchesSelector2("li", element)); // true
console.log(matchesSelector2(".container *", element)); // true

You can see this working at jsfiddle.

Putting it all together

Here's the final implementation I came up with:

function is(element, selector) {
  var node = element;
  var result = false;
  var root, frag;

  // crawl up the tree
  while (node.parentNode) {
    node = node.parentNode;
  }

  // root must be either a Document or a DocumentFragment
  if (node instanceof Document || node instanceof DocumentFragment) {
    root = node;
  } else {
    root = frag = document.createDocumentFragment();
    frag.appendChild(node);
  }

  // see if selector matches
  var matches = root.querySelectorAll(selector);
  for (var i = 0; i < matches.length; i++) {
    if (this === matches.item(i)) {
      result = true;
      break;
    }
  }

  // detach from DocumentFragment and return result
  while (frag && frag.firstChild) {
    frag.removeChild(frag.firstChild);
  }
  return result;
}

An important note is that jQuery's is implementation is much faster. The first optimization I would look into is avoiding crawling up the tree if we don't have to. To do this you could look at the right-most part of the selector and test whether this matches the element. However, beware that if the selector is actually multiple selectors separated by commas, then you'll have to test each one. At this point you're building a CSS selector parser, so you might as well use a library.