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.
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.
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>
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
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.
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.
In the absence of xMatchesSelector
, I'm thinking to try adding a style with the requested selector to a styleSheet
object, along with some arbitrary rule and value that is not likely to be already in use. Then check the computed/currentStyle
of the element to see if it has inherited the added CSS rule. Something like this for IE:
function ieMatchesSelector(selector, element) {
var styleSheet = document.styleSheets[document.styleSheets.length-1];
//arbitrary value, probably should first check
//on the off chance that it is already in use
var expected = 91929;
styleSheet.addRule(selector, 'z-index: '+expected+' !important;', -1);
var result = element.currentStyle.zIndex == expected;
styleSheet.removeRule(styleSheet.rules.length-1);
return result;
}
There's probably a handbag full of gotcha's with this method. Probably best to find some obscure proprietary CSS rule that is less likely to have a visual effect than z-index
, but since it is removed almost immediately after it is set, a brief flicker should be the only side effect if that. Also a more obscure rule will be less likely to be overridden by a more specific selector, style attribute rules, or other !important rules (if IE even supports that). Anyway, worth a try at least.
The W3C selectors API (http://www.w3.org/TR/selectors-api/) specifies document.querySelectorAll()
. This is not supported on all browsers, so you'd have to google the ones that do support it: http://www.google.com/search?q=browsers+implementing+selector+api
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With