jQuery’s closest() in pure JS

December 19th, 2014 | Permalink

Imagine you have an element and you need to find it’s first parent that matches some selector.

Say, I had a plenty of elements having ‘div.audio’ element inside and i needed to find those who do. Of course, I could iterate through all of them and check if they contain such elements, but that would mean too much redundant querySelectorAll calls. So i decided to replace that with too much redundant .matches calls.

So. First of all i had to select all the .audio elements within .post elements. To avoid doubling (to check only one .audio element per .post element) i used  :first-child

var audioElements = [].slice.call(document.querySelectorAll('.post .audio:first-child'));

Then i had them arrayed via [].slice.call I did this in Chrome (whole thing is done for Chrome extension), but hopefully it should work in all decent browsers.

So then I wrote this function that climbs up the DOM till it finds the element matching the given selector:

Element.prototype.parentThatMatches = function(selector){
  var parent = this.parentNode;
  if(parent && parent instanceof Element) {
    if (this.parentNode.matches(selector))return this.parentNode;
    else return this.parentNode.parentThatMatches(selector);
  }
  return null;
};

The function above uses ‘matches’ function, which is currently available only in chrome, I guess. So for other browsers we might need a fallback (fortunately, most browsers have own .matches function, only it’s prefixed):

if(!Element.prototype.matches)Element.prototype.matches = function(selector) {
  return (this.matchesSelector || this.msMatchesSelector || this.mozMatchesSelector || this.webkitMatchesSelector || this.oMatchesSelector).call(this, selector);
};

And, obfuscated a bit, that would look something like that:

(function(b){b.matches||["","ms","moz","webkit","o"].forEach(function(a){a=a+(a?"M":"m")+"atchesSelector";b[a]&&(b.matches=b[a])})})(Element.prototype);

Now, using the function we can form an array of .post elements from the array of .audio elements:

var posts = []
for(var index in audioElements) {
    posts.push (audioElements[index].parentThatMatches('.post'));
}