Closest ancestor matching selector using native DOM? - javascript

Is anybody working on a jQuery.closest() equivalent in the DOM api?
Looks like the Selectors Level 2 draft adds matches() equivalent to jQuery.is(), so native closest should be much easier to write. Has adding closest() to Selectors come up?

Building off of Alnitak's answer. Here's the working current implementation with matchesSelector which is now matches in the DOM spec.
// get nearest parent element matching selector
function closest(el, selector) {
var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
while (el) {
if (matchesSelector.call(el, selector)) {
break;
}
el = el.parentElement;
}
return el;
}
Browser support is great: http://caniuse.com/matchesselector

Seems like Chrome 40 will bring a native element.closest() method (http://blog.chromium.org/2014/12/chrome-40-beta-powerful-offline-and.html) specified here: https://dom.spec.whatwg.org/#dom-element-closest

See the element.closest() documentation.
Implementing such function with Element.matches() seems not optimal in terms of performance, cause apparently matches() will make a call to querySelectorAll() every time you test a parent, while only one call is sufficient for the job.
Here's a polyfill for closest() on MDN. Note a single call to querySelectorAll()
if (window.Element && !Element.prototype.closest) {
Element.prototype.closest =
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i,
el = this;
do {
i = matches.length;
while (--i >= 0 && matches.item(i) !== el) {};
} while ((i < 0) && (el = el.parentElement));
return el;
};
}
But bear in mind that function implemented like this will not work properly on unattached tree (detached from document.documentElement root)
//Element.prototype.closestTest = function(s){...as seen above...};
var detachedRoot = document.createElement("footer");
var child = detachedRoot.appendChild(document.createElement("div"));
detachedRoot.parentElement; //null
child.closestTest("footer"); //null
document.documentElement.append(detachedRoot);
child.closestTest("footer"); //<footer>
Though closest() that is implemented in Firefox 51.0.1 seems to work fine with detached tree
document.documentElement.removeChild(detachedRoot);
child.closestTest("footer"); //null
child.closest("footer"); //<footer>

This sounds like it ought to be pretty easy, given the matches function, although that's not widely supported yet:
function closest(elem, selector) {
while (elem) {
if (elem.matches(selector)) {
return elem;
} else {
elem = elem.parentElement;
}
}
return null;
}
The trouble is, the matches function isn't properly supported. As it's still a relatively new API it's available as webkitMatchesSelector in Chrome and Safari, and mozMatchesSelector in Firefox.

Using element.closest() we can find Closest ancestor matching selector. This method takes selectors list as parameter and returns the closest ancestor. As per Rob's Comment this API will be available from chrome 41 and FF 35.
As explained in whatwg specs https://dom.spec.whatwg.org/#dom-element-closest
Example: The below HTML will show alert message "true"
<html>
<body>
<foo>
<bar>
<a id="a">
<b id="b">
<c id="c"></c>
</b>
</a>
</bar>
</foo>
<script>
var a = document.getElementById('a');
var b = document.getElementById('b');
var c = document.getElementById('c');
alert(c.closest("a, b")==b);
</script>
</body>
</html>

A little recursion will do the trick.
// get nearest parent element matching selector
var closest = (function() {
var matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
return function closest(el, selector) {
return !el ? null :
matchesSelector.call(el, selector) ? el : closest(el.parentElement, selector);
};
})();

Related

How ot select an element in the DOM hierarchy that has a specific class? [duplicate]

How can I find an element's ancestor that is closest up the tree that has a particular class, in pure JavaScript? For example, in a tree like so:
<div class="far ancestor">
<div class="near ancestor">
<p>Where am I?</p>
</div>
</div>
Then I want div.near.ancestor if I try this on the p and search for ancestor.
Update: Now supported in most major browsers
document.querySelector("p").closest(".near.ancestor")
Note that this can match selectors, not just classes
https://developer.mozilla.org/en-US/docs/Web/API/Element.closest
For legacy browsers that do not support closest() but have matches() one can build selector-matching similar to #rvighne's class matching:
function findAncestor (el, sel) {
while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
return el;
}
This does the trick:
function findAncestor (el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
}
The while loop waits until el has the desired class, and it sets el to el's parent every iteration so in the end, you have the ancestor with that class or null.
Here's a fiddle, if anyone wants to improve it. It won't work on old browsers (i.e. IE); see this compatibility table for classList. parentElement is used here because parentNode would involve more work to make sure that the node is an element.
Use element.closest()
https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
See this example DOM:
<article>
<div id="div-01">Here is div-01
<div id="div-02">Here is div-02
<div id="div-03">Here is div-03</div>
</div>
</div>
</article>
This is how you would use element.closest:
var el = document.getElementById('div-03');
var r1 = el.closest("#div-02");
// returns the element with the id=div-02
var r2 = el.closest("div div");
// returns the closest ancestor which is a div in div, here is div-03 itself
var r3 = el.closest("article > div");
// returns the closest ancestor which is a div and has a parent article, here is div-01
var r4 = el.closest(":not(div)");
// returns the closest ancestor which is not a div, here is the outmost article
Based on the the8472 answer and https://developer.mozilla.org/en-US/docs/Web/API/Element/matches here is cross-platform 2017 solution:
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
function findAncestor(el, sel) {
if (typeof el.closest === 'function') {
return el.closest(sel) || null;
}
while (el) {
if (el.matches(sel)) {
return el;
}
el = el.parentElement;
}
return null;
}
#rvighne solution works well, but as identified in the comments ParentElement and ClassList both have compatibility issues. To make it more compatible, I have used:
function findAncestor (el, cls) {
while ((el = el.parentNode) && el.className.indexOf(cls) < 0);
return el;
}
parentNode property instead of the parentElement property
indexOf method on the className property instead of the contains method on the classList property.
Of course, indexOf is simply looking for the presence of that string, it does not care if it is the whole string or not. So if you had another element with class 'ancestor-type' it would still return as having found 'ancestor', if this is a problem for you, perhaps you can use regexp to find an exact match.
This solution should work for IE9 and up.
It's like jQuery's parents() method when you need to get a parent container which might be up a few levels from the given element, like finding the containing <form> of a clicked <button>. Looks through the parents until the matching selector is found, or until it reaches the <body>. Returns either the matching element or the <body>.
function parents(el, selector){
var parent_container = el;
do {
parent_container = parent_container.parentNode;
}
while( !parent_container.matches(selector) && parent_container !== document.body );
return parent_container;
}

How to check if an element matches the :empty pseudo-class in pure JS?

How can I check, in pure JavaScript (no jQuery, no libraries), if a given HTML element is empty? My definition of "empty" is the same as the CSS :empty pseudo-class. So, if a given element would match the :empty selector, then I want to know about it.
function isEmpty (el) {
if (!el.hasChildNodes()) return true;
for (var node = el.firstChild; node = node.nextSibling;) {
var type = node.nodeType;
if (type === 1 && !isEmpty(node) || // another element
type === 3 && node.nodeValue) { // text node
return false;
}
}
return true;
}
As per the CSS spec, this will return true if the given element has no non-empty child nodes. Long-awaited JSFiddle demo available.
A way to ensure spec compliance is to use .querySelectorAll or .matches.
function matches(el, selector) {
return !!~[].indexOf.call(el.ownerDocument.querySelectorAll(selector));
}
Depending on browser support needs, .matches is more direct and even works on detached nodes:
function matches(el, selector) {
return !!(el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector || el.oMatchesSelector).call(el, selector);
}
Use either of those like:
matches(el, ':empty')
As far as I know, jQuery's implementation of :empty matches the spec exactly, so it can be used as a reference. From the latest version as of this writing:
"empty": function( elem ) {
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
// not comment, processing instructions, or others
// Thanks to Diego Perini for the nodeName shortcut
// Greater than "#" means alpha characters (specifically not starting with "#" or "?")
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
if ( elem.nodeName > "#" || elem.nodeType === 3 || elem.nodeType === 4 ) {
return false;
}
}
return true;
}
I think the simplest and easiest way to do this is :
elem = document.getElemntById('id_of_the_elem');
function isEmpty(elem){
if(elem.innerHTML=='') console.log("empty tag");
else console.log('Non empty tag');
}

Find the closest ancestor element that has a specific class

How can I find an element's ancestor that is closest up the tree that has a particular class, in pure JavaScript? For example, in a tree like so:
<div class="far ancestor">
<div class="near ancestor">
<p>Where am I?</p>
</div>
</div>
Then I want div.near.ancestor if I try this on the p and search for ancestor.
Update: Now supported in most major browsers
document.querySelector("p").closest(".near.ancestor")
Note that this can match selectors, not just classes
https://developer.mozilla.org/en-US/docs/Web/API/Element.closest
For legacy browsers that do not support closest() but have matches() one can build selector-matching similar to #rvighne's class matching:
function findAncestor (el, sel) {
while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
return el;
}
This does the trick:
function findAncestor (el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
}
The while loop waits until el has the desired class, and it sets el to el's parent every iteration so in the end, you have the ancestor with that class or null.
Here's a fiddle, if anyone wants to improve it. It won't work on old browsers (i.e. IE); see this compatibility table for classList. parentElement is used here because parentNode would involve more work to make sure that the node is an element.
Use element.closest()
https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
See this example DOM:
<article>
<div id="div-01">Here is div-01
<div id="div-02">Here is div-02
<div id="div-03">Here is div-03</div>
</div>
</div>
</article>
This is how you would use element.closest:
var el = document.getElementById('div-03');
var r1 = el.closest("#div-02");
// returns the element with the id=div-02
var r2 = el.closest("div div");
// returns the closest ancestor which is a div in div, here is div-03 itself
var r3 = el.closest("article > div");
// returns the closest ancestor which is a div and has a parent article, here is div-01
var r4 = el.closest(":not(div)");
// returns the closest ancestor which is not a div, here is the outmost article
Based on the the8472 answer and https://developer.mozilla.org/en-US/docs/Web/API/Element/matches here is cross-platform 2017 solution:
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
function findAncestor(el, sel) {
if (typeof el.closest === 'function') {
return el.closest(sel) || null;
}
while (el) {
if (el.matches(sel)) {
return el;
}
el = el.parentElement;
}
return null;
}
#rvighne solution works well, but as identified in the comments ParentElement and ClassList both have compatibility issues. To make it more compatible, I have used:
function findAncestor (el, cls) {
while ((el = el.parentNode) && el.className.indexOf(cls) < 0);
return el;
}
parentNode property instead of the parentElement property
indexOf method on the className property instead of the contains method on the classList property.
Of course, indexOf is simply looking for the presence of that string, it does not care if it is the whole string or not. So if you had another element with class 'ancestor-type' it would still return as having found 'ancestor', if this is a problem for you, perhaps you can use regexp to find an exact match.
This solution should work for IE9 and up.
It's like jQuery's parents() method when you need to get a parent container which might be up a few levels from the given element, like finding the containing <form> of a clicked <button>. Looks through the parents until the matching selector is found, or until it reaches the <body>. Returns either the matching element or the <body>.
function parents(el, selector){
var parent_container = el;
do {
parent_container = parent_container.parentNode;
}
while( !parent_container.matches(selector) && parent_container !== document.body );
return parent_container;
}

Custom Javascript href selector

With jquery it's rather easy to use a selector which url starts with x. Something like that:
$("a[href*=#test]").click(function(e) {
e.preventDefault();
alert('works');
});
Is there equivalent to it in pure javascript? Or what would be the easiest way to do it?
I have found getElementsByName and getElementsByClassName but what about this case?
I think querySelectorAll should do the trick if you don't need to support IE < 8 (http://caniuse.com/queryselector)
There's no equivalent in the older Javascript spec, so you can't use querySelectorAll, and still support older browsers like <IE8.
What you'd have to do is use getElementsByTagName, and then filter the results by checking each one's href property. If you check the JQuery source, I think you'll find it does just that, more or less.
You can always use newer features like querySelectorAll, and include a "polyfill" to add support for older browsers. Here's an example.
if (!document.querySelectorAll) {
document.querySelectorAll = function(selector) {
var doc = document,
head = doc.documentElement.firstChild,
styleTag = doc.createElement('STYLE');
head.appendChild(styleTag);
doc.__qsaels = [];
styleTag.styleSheet.cssText = selector + "{x:expression(document.__qsaels.push(this))}";
window.scrollBy(0, 0);
return doc.__qsaels;
}
}
This article has some great information on this.
http://remysharp.com/2013/04/19/i-know-jquery-now-what/
Pure Javascript
var $ = document.querySelectorAll.bind(document);
Element.prototype.on = Element.prototype.addEventListener;
$('#somelink')[0].on('touchstart', handleTouch);
But I don't think it supports old IE and you may not be able to do the selector your desire
However,
If your just looking for something lightweight you can use the sizzle engine on its own, without jquery.
Only weighs 4k.
//Only searches anchor tags
function getElementsByHref(href) {
var els = document.getElementsByTagName("a");
var result = [];
for (i=0;i<els.length;i++) {
if (els[i].getAttribute("href") == href) result.push(els[i]);
}
return result;
}
You can do it with document.querySelector (or querySelectorAll)
If you want a link whose href starts with 'test' you can use -
document.querySelectorAll("a[href^=test]');
var hyperlinks = document.getElementsByTagName("a") would return all hyerlinks within your document. You can then loop over those results and for each element check the value of the href attribute via element.getAttribute("href") and check if that string value starts with the desired string, if it does then bind a click event to that element.
You can also use this procedure:
var a = document.getElementsByTagName('a');
for (var i=0; i<a.length; i++){
if ((a[i].id && a[i].id.toLowerCase().indexOf('test') !== 0) {
continue; // id not starts with 'test'
}
if (a[i].addEventListener) {
a[i].addEventListener('click', handler, false);
} else {
a[i].attachEvent('onclick', handler);
}
}
function handler(e){
e = e || window.event; // for IE8/7 backward compatibility
if (e.preventDefault) {
e.preventDefault();
} else {
event.returnValue = false; // for IE8/7 backward compatibility
}
alert('works');
}
http://jsfiddle.net/VcVpM/19/ (code)
http://jsfiddle.net/VcVpM/19/show (result page)

Hide an element's next sibling with Javascript

I have an element grabbed from document.getElementById('the_id'). How can I get its next sibling and hide it? I tried this but it didn't work:
elem.nextSibling.style.display = 'none';
Firebug error was elem.nextSibling.style is undefined.
it's because Firefox considers the whitespace between element nodes to be text nodes (whereas IE does not) and therefore using .nextSibling on an element gets that text node in Firefox.
It's useful to have a function to use to get the next element node. Something like this
/*
Credit to John Resig for this function
taken from Pro JavaScript techniques
*/
function next(elem) {
do {
elem = elem.nextSibling;
} while (elem && elem.nodeType !== 1);
return elem;
}
then you can do
var elem = document.getElementById('the_id');
var nextElem = next(elem);
if (nextElem)
nextElem.style.display = 'none';
Take a look at the Element Traversal API, that API moves between Element nodes only. This Allows the following:
elem.nextElementSibling.style.display = 'none';
And thus avoids the problem inherent in nextSibling of potentially getting non-Element nodes (e.g. TextNode holding whitespace)
Firebug error was elem.nextSibling.style is undefined.
because nextSibling can be a text-node or other node type
do {
elem = elem.nextSibling;
} while(element && elem.nodeType !== 1); // 1 == Node.ELEMENT_NODE
if(elem) elem.style.display = 'none';
Try looping through the children of this element using something like:
var i=0;
(foreach child in elem)
{
if (i==0)
{
document.getElementByID(child.id).style.display='none';
}
}
Please make appropriate corrections to the syntax.

Categories

Resources