Trouvez l'élément ancêtre le plus proche qui a une classe spécifique

225

Comment puis-je trouver l'ancêtre d'un élément le plus proche de l'arbre qui a une classe particulière, en pur JavaScript ? Par exemple, dans un arbre comme celui-ci:

<div class="far ancestor">
    <div class="near ancestor">
        <p>Where am I?</p>
    </div>
</div>

Alors je veux div.near.ancestorsi j'essaye ceci sur le pet cherche ancestor.

rvighne
la source
1
veuillez noter que le div externe n'est pas un parent, c'est un ancêtre de l' pélément. Si vous ne souhaitez réellement obtenir que le nœud parent, vous pouvez le faire ele.parentNode.
Felix Kling
@FelixKling: ne connaissait pas cette terminologie; Je vais le changer.
rvighne
3
C'est en fait la même chose que nous les humains utilisons :) Le père (parent) de votre père (parent) n'est pas votre père (parent), c'est votre grand-père (grand-parent), ou plus généralement, votre ancêtre.
Felix Kling

Réponses:

346

Mise à jour: désormais prise en charge dans la plupart des principaux navigateurs

document.querySelector("p").closest(".near.ancestor")

Notez que cela peut correspondre aux sélecteurs, pas seulement aux classes

https://developer.mozilla.org/en-US/docs/Web/API/Element.closest


Pour les navigateurs hérités qui ne prennent pas en charge closest()mais qui en ont matches()un, vous pouvez créer une correspondance de sélecteur similaire à la correspondance de classe de @ rvighne:

function findAncestor (el, sel) {
    while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
    return el;
}
the8472
la source
1
Toujours pas pris en charge dans les versions actuelles d'Internet Explorer, Edge et Opera Mini.
kleinfreund
1
@kleinfreund - toujours pas pris en charge dans IE, Edge ou Opera mini. caniuse.com/#search=closest
evolutionxbox
8
Juin 2016: il semble que tous les navigateurs n'aient pas encore rattrapé leur retard
Kunal
3
Il existe un polyfill DOM niveau 4 avec en closestplus de nombreuses fonctionnalités de traversée DOM plus avancées. Vous pouvez extraire cette implémentation seule ou inclure l'ensemble complet pour obtenir de nombreuses nouvelles fonctionnalités dans votre code.
Garbee
2
Edge 15 a un support, qui devrait couvrir tous les principaux navigateurs. Sauf si vous comptez IE11, mais qui ne reçoit aucune mise à jour
the8472
195

Cela fait l'affaire:

function findAncestor (el, cls) {
    while ((el = el.parentElement) && !el.classList.contains(cls));
    return el;
}

La boucle while attend jusqu'à ce qu'elle elait la classe souhaitée, et elle est définie elsur elle parent de chaque itération, donc à la fin, vous avez l'ancêtre avec cette classe ou null.

Voici un violon , si quelqu'un veut l'améliorer. Cela ne fonctionnera pas sur les anciens navigateurs (ie IE); voir ce tableau de compatibilité pour classList . parentElementest utilisé ici car parentNodecela impliquerait plus de travail pour s'assurer que le nœud est un élément.

rvighne
la source
1
Pour une alternative à .classList, voir stackoverflow.com/q/5898656/218196 .
Felix Kling
1
J'ai corrigé le code, mais cela générerait toujours une erreur s'il n'y a pas d'ancêtre avec un tel nom de classe.
Felix Kling
2
Euh, je pensais que ça n'existait pas, mais j'avais tort . Quoi qu'il en soit, parentElementest une propriété plutôt nouvelle (DOM niveau 4) et parentNodeexiste depuis ... pour toujours. Fonctionne donc parentNodeégalement dans les navigateurs plus anciens, alors parentElementque non. Bien sûr, vous pourriez faire le même argument pour / contre classList, mais il n'a pas d'alternative simple, comme parentElementhas. En fait, je me demande pourquoi parentElementexiste-t-il, cela ne semble pas ajouter de valeur parentNode.
Felix Kling
1
@FelixKling: Juste édité, cela fonctionne maintenant très bien dans le cas où aucun ancêtre n'existe J'ai dû devenir exagéré avec la version originale courte.
rvighne
3
Si vous souhaitez vérifier le nom de classe dans l'élément de paramètre lui-même, avant de rechercher ses ancêtres, vous pouvez simplement changer l'ordre des conditions dans la boucle:while (!el.classList.contains(cls) && (el = el.parentElement));
Nicomak
34

Utilisez element.closest ()

https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

Voir cet exemple 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>

Voici comment vous utiliseriez 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
simbro
la source
14

Sur la base de la réponse the8472 et https://developer.mozilla.org/en-US/docs/Web/API/Element/matches voici la solution multiplateforme 2017:

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;
}
marverix
la source
11

La solution @rvighne fonctionne bien, mais comme identifié dans les commentaires ParentElementet les ClassListdeux ont des problèmes de compatibilité. Pour le rendre plus compatible, j'ai utilisé:

function findAncestor (el, cls) {
    while ((el = el.parentNode) && el.className.indexOf(cls) < 0);
    return el;
}
  • parentNodepropriété au lieu de la parentElementpropriété
  • indexOfsur la classNamepropriété au lieu de la containsméthode sur la classListpropriété.

Bien sûr, indexOf recherche simplement la présence de cette chaîne, peu importe si c'est la chaîne entière ou non. Donc, si vous aviez un autre élément avec la classe 'ancestor-type', il retournerait toujours comme ayant trouvé 'ancestor', si cela vous pose problème, vous pouvez peut-être utiliser regexp pour trouver une correspondance exacte.

Josh
la source