Encontre o elemento ancestral mais próximo que tenha uma classe específica

225

Como posso encontrar o ancestral de um elemento mais próximo da árvore que possui uma classe específica, em JavaScript puro ? Por exemplo, em uma árvore como esta:

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

Então eu quero div.near.ancestorse eu tentar isso no pe procurar ancestor.

rvighne
fonte
1
observe que a div externa não é um pai, é um ancestral do pelemento. Se você realmente deseja apenas obter o nó pai, pode fazê-lo ele.parentNode.
Felix Kling
@ FelixKling: Não conhecia essa terminologia; Eu mudarei isso.
rvighne
3
Na verdade, é o mesmo que nós, seres humanos usamos :) O pai (pai) de seu pai (pai) não é seu pai (pai), é seu avô (avô) ou, em termos mais gerais, seu ancestral.
Felix Kling

Respostas:

346

Atualização: agora suportada na maioria dos principais navegadores

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

Observe que isso pode corresponder aos seletores, não apenas às classes

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


Para navegadores herdados que não oferecem suporte, closest()mas matches()podem criar correspondência de seletor semelhante à correspondência de classe do @ rvighne:

function findAncestor (el, sel) {
    while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
    return el;
}
the8472
fonte
1
Ainda não há suporte nas versões atuais do Internet Explorer, Edge e Opera Mini.
kleinfreund
1
@kleinfreund - ainda não é suportado no IE, Edge ou Opera mini. caniuse.com/#search=closest
evolutionxbox
8
Junho de 2016: parece que todos os navegadores ainda não foram atualizados
Kunal
3
Existe um polyfill do DOM nível 4 com closestmuitos outros recursos avançados de travessia do DOM. Você pode executar essa implementação por conta própria ou incluir todo o pacote para obter muitos novos recursos em seu código.
Garbee
2
O Edge 15 tem suporte, que deve abranger todos os principais navegadores. A menos que você conte IE11, mas que não recebe nenhuma atualização
the8472
195

Isso faz o truque:

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

O loop while espera até elter a classe desejada e define elcomo elpai a cada iteração. No final, você tem o ancestral dessa classe ou null.

Aqui está um violino , se alguém quiser melhorá-lo. Não funcionará em navegadores antigos (ie IE); consulte esta tabela de compatibilidade para classList . parentElementé usado aqui porque parentNodeenvolveria mais trabalho para garantir que o nó seja um elemento.

rvighne
fonte
1
Para uma alternativa .classList, consulte stackoverflow.com/q/5898656/218196 .
Felix Kling
1
Corrigi o código, mas ainda geraria um erro se não houvesse um ancestral com esse nome de classe.
Felix Kling
2
Achei que não existia, mas estava errado . De qualquer forma, parentElementé uma propriedade bastante nova (nível 4 do DOM) e parentNodeexiste desde ... para sempre. O mesmo parentNodetambém funciona em navegadores mais antigos, ao passo que parentElementtalvez não. Claro que você poderia argumentar a favor / contra classList, mas ele não tem uma alternativa simples, como parentElementtem. Na verdade, eu me pergunto por que parentElementexiste, não parece agregar nenhum valor parentNode.
Felix Kling #
1
@ FelixKling: Acabado de editar, agora funciona bem para o caso em que não existe esse ancestral. Devo ter ficado exagerado com a versão original curta.
rvighne
3
Se você deseja verificar o nome da classe no próprio elemento de parâmetro, antes de pesquisar seus ancestrais, pode simplesmente mudar a ordem das condições no loop:while (!el.classList.contains(cls) && (el = el.parentElement));
Nicomak
34

Use element.closest ()

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

Veja este exemplo 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>

É assim que você usaria o 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
fonte
14

Com base na resposta the8472 e https://developer.mozilla.org/en-US/docs/Web/API/Element/matches, aqui está a solução 2017 para várias plataformas:

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
fonte
11

A solução @rvighne funciona bem, mas conforme identificado nos comentários ParentElemente ClassListambos têm problemas de compatibilidade. Para torná-lo mais compatível, eu usei:

function findAncestor (el, cls) {
    while ((el = el.parentNode) && el.className.indexOf(cls) < 0);
    return el;
}
  • parentNodepropriedade em vez da parentElementpropriedade
  • indexOfmétodo na classNamepropriedade em vez do containsmétodo na classListpropriedade

Obviamente, o indexOf está simplesmente procurando a presença dessa string, não importa se é a string inteira ou não. Portanto, se você tivesse outro elemento com a classe 'tipo de ancestral', ele ainda retornaria como tendo encontrado 'ancestral', se esse for um problema para você, talvez você possa usar o regexp para encontrar uma correspondência exata.

Josh
fonte