Semelhante ao jQuery .closest (), mas atravessando descendentes?

123

Existe uma função semelhante a jQuery .closest()mas para atravessar descendentes e retornar apenas os mais próximos?

Eu sei que existe .find()função, mas ele retorna todas as correspondências possíveis, não as mais próximas.

Editar:

Aqui está a definição de mais próximo (pelo menos para mim) :

Em primeiro lugar, todas as crianças são atravessadas, depois as crianças de cada indivíduo são atravessadas.

No exemplo abaixo, id='2'estão os .closestdescendentes mais próximos deid="find-my-closest-descendant"

<div id="find-my-closest-descendant">
    <div>
        <div class="closest" Id='1'></div>
    </div>
    <div class="closest" Id='2'></div>
</div>

Por favor, veja o link JSfiddle .

mamu
fonte
4
Que tal .find("filter here").eq(0)?
Shadow Wizard é Ear For You
@ShadowWizard bem, se fizer uma travessia de profundidade primeiro, o zeroésimo elemento na lista de resultados poderá não ser o "mais próximo", dependendo do significado de "mais próximo".
Pointy
@ Pointy não é o mesmo que :firstfiltro?
Shadow Wizard é Ear For You
Não, acho que não. O problema é que ": first" e ".eq (0)" se referem à ordem DOM, mas se "mais próximo" significa "menos nós DOM", então um "neto" do o primeiro filho seria retornado em vez de um "filho" posterior que corresponda ao seletor. Não tenho 100% de certeza, é disso que trata o OP, é claro.
Pointy
1
Existe um plugin jQuery que exatamente fazer isso por você: plugins.jquery.com/closestDescendant
TLindig

Respostas:

44

De acordo com a sua definição de closest, escrevi o seguinte plugin:

(function($) {
    $.fn.closest_descendent = function(filter) {
        var $found = $(),
            $currentSet = this; // Current place
        while ($currentSet.length) {
            $found = $currentSet.filter(filter);
            if ($found.length) break;  // At least one match: break loop
            // Get all children of the current set
            $currentSet = $currentSet.children();
        }
        return $found.first(); // Return first match of the collection
    }    
})(jQuery);
Rob W
fonte
3
Diferentemente da .closest()função jQuery , ela também corresponde ao elemento em que foi chamada. Veja meu jsfiddle . Alterando-o para $currentSet = this.children(); // Current placeele vai começar com ele os filhos em vez jsFiddle
allicarn
21
O mais próximo () do JQuery também pode ser comparado ao elemento atual.
Dávid Horváth
120

Se por descendente "mais próximo" você quer dizer o primeiro filho, pode:

$('#foo').find(':first');

Ou:

$('#foo').children().first();

Ou, para procurar a primeira ocorrência de um elemento específico, você pode fazer:

$('#foo').find('.whatever').first();

Ou:

$('#foo').find('.whatever:first');

Realmente, porém, precisamos de uma definição sólida do que significa "descendente mais próximo".

Por exemplo

<div id="foo">
    <p>
        <span></span>
    </p>
    <span></span>
</div>

Que <span>iria $('#foo').closestDescendent('span')voltar?

James
fonte
1
obrigado @ 999 pela resposta detalhada, minha expectativa de falecido é primeiro todas as crianças atravessadas, depois as crianças de cada indivíduo. No exemplo que você deu, o segundo intervalo seria retornado
mamu
8
Então, primeiro a respiração, não a profundidade primeiro
jessehouwing
1
Excelente resposta. Eu estaria interessado em saber se $('#foo').find('.whatever').first();é "em largura" ou "em profundidade"
Colin
1
O único problema com esta solução é que o jQuery encontrará TODOS os critérios correspondentes e retornará o primeiro. Enquanto o mecanismo Sizzle é rápido, isso representa uma pesquisa extra desnecessária. Não há como interromper a busca e parar após a primeira correspondência ser encontrada.
icfantv
Todos esses exemplos selecionam descendentes em profundidade, e não em largura, portanto, eles não podem ser usados ​​para resolver a questão original. Nos meus testes, todos retornaram o primeiro intervalo, não o segundo.
JrBaconCheez
18

Você pode usar findcom o :firstseletor:

$('#parent').find('p:first');

A linha acima encontrará o primeiro <p>elemento nos descendentes de #parent.

FishBasketGordo
fonte
2
Eu acho que é isso que o OP queria, nenhum plugin é necessário. Isso selecionará o primeiro elemento correspondente nos descendentes do elemento selecionado, para cada elemento selecionado. Portanto, se você tiver 4 correspondências com sua seleção original, poderá terminar com 4 correspondências usando o find('whatever:first').
RustyToms
3

E essa abordagem?

$('find-my-closest-descendant').find('> div');

Esse seletor "filho direto" funciona para mim.

Klawipo
fonte
o OP pede especificamente algo que atravesse descendentes, o que esta resposta não. Essa pode ser uma boa resposta, mas não para essa pergunta em particular.
Jumps4fun
@KjetilNordin Talvez seja porque a pergunta foi editada desde a minha resposta. Além disso, a definição de descendente mais próximo ainda não é tão clara, pelo menos para mim.
precisa saber é
Eu definitivamente concordo. descendente mais próximo pode significar muitas coisas. Mais fácil com o pai nesse caso, onde sempre há um pai único para os elementos, em cada nível. E você também está certo sobre possíveis edições. Vejo agora que meu comentário não foi útil de forma alguma, especialmente porque já havia uma declaração do mesmo tipo.
Jumps4fun 22/08/19
1

Solução JS pura (usando ES6).

export function closestDescendant(root, selector) {
  const elements = [root];
  let e;
  do { e = elements.shift(); } while (!e.matches(selector) && elements.push(...e.children));
  return e.matches(selector) ? e : null;
}

Exemplo

Considerando a seguinte estrutura:

div == $ 0
Div── div == $ 1
│ ├── div
Div ├── div.findme == $ 4
│ ├── div
│ └── div
Div── div.findme == $ 2
│ ├── div
│ └── div
└── div == $ 3
    ├── div
    ├── div
    └── div
closestDescendant($0, '.findme') === $2;
closestDescendant($1, '.findme') === $4;
closestDescendant($2, '.findme') === $2;
closestDescendant($3, '.findme') === null;

ccjmne
fonte
Esta solução funciona, mas eu publiquei uma solução ES6 alternativa aqui ( stackoverflow.com/a/55144581/2441655 ), pois eu não gosto: 1) A whileexpressão com efeitos colaterais. 2) Uso da .matchesverificação em duas linhas, em vez de apenas uma.
Venryx
1

Caso alguém esteja procurando por uma solução JS pura (usando ES6 em vez de jQuery), eis a que eu uso:

Element.prototype.QuerySelector_BreadthFirst = function(selector) {
    let currentLayerElements = [...this.childNodes];
    while (currentLayerElements.length) {
        let firstMatchInLayer = currentLayerElements.find(a=>a.matches && a.matches(selector));
        if (firstMatchInLayer) return firstMatchInLayer;
        currentLayerElements = currentLayerElements.reduce((acc, item)=>acc.concat([...item.childNodes]), []);
    }
    return null;
};
Venryx
fonte
Ei, obrigado por trazer sua resposta à minha atenção! Você está certo, olhando para mim mesma, parece que está tentando ser muito inteligente e realmente estranho (correr matchesduas vezes também me irrita um pouco). + 1 para você :) #
2213 ccjmne
0

Eu acho que primeiro você precisa definir o que significa "mais próximo". Se você quer dizer que o nó descendente que corresponde aos seus critérios está a menor distância em termos de links dos pais, usar ": first" ou ".eq (0)" não funcionará necessariamente:

<div id='start'>
  <div>
    <div>
      <span class='target'></span>
    </div>
  </div>
  <div>
    <span class='target'></span>
  </div>
</div>

Nesse exemplo, o segundo <span>elemento ".target" está "mais próximo" do "start" <div>, porque fica a apenas um salto dos pais. Se é isso que você quer dizer com "mais próximo", você precisa encontrar a distância mínima em uma função de filtro. A lista de resultados de um seletor jQuery está sempre na ordem DOM.

Talvez:

$.fn.closestDescendant = function(sel) {
  var rv = $();
  this.each(function() {
    var base = this, $base = $(base), $found = $base.find(sel);
    var dist = null, closest = null;
    $found.each(function() {
      var $parents = $(this).parents();
      for (var i = 0; i < $parents.length; ++i)
        if ($parents.get(i) === base) break;
      if (dist === null || i < dist) {
        dist = i;
        closest = this;
      }
    });
    rv.add(closest);
  });
  return rv;
};

Esse é um plug-in de hackers, devido à maneira como ele cria o objeto de resultado, mas a idéia é que você precisa encontrar o caminho mais curto dos pais entre todos os elementos correspondentes que encontrar. Esse código é direcionado para os elementos à esquerda na árvore DOM devido à <verificação; <=viés para a direita.

Pontudo
fonte
0

Eu preparei isso, nenhuma implementação para seletores de posição (eles precisam de mais do que apenas matchesSelector) ainda:

Demonstração: http://jsfiddle.net/TL4Bq/3/

(function ($) {
    var matchesSelector = jQuery.find.matchesSelector;
    $.fn.closestDescendant = function (selector) {
        var queue, open, cur, ret = [];
        this.each(function () {
            queue = [this];
            open = [];
            while (queue.length) {
                cur = queue.shift();
                if (!cur || cur.nodeType !== 1) {
                    continue;
                }
                if (matchesSelector(cur, selector)) {
                    ret.push(cur);
                    return;
                }
                open.unshift.apply(open, $(cur).children().toArray());
                if (!queue.length) {
                    queue.unshift.apply(queue, open);
                    open = [];
                }
            }
        });
        ret = ret.length > 1 ? jQuery.unique(ret) : ret;
        return this.pushStack(ret, "closestDescendant", selector);
    };
})(jQuery);

Provavelmente existem alguns erros, mas não o testaram muito.

Esailija
fonte
0

A resposta de Rob W não funcionou muito bem para mim. Eu o adaptei a isso que funcionou.

//closest_descendent plugin
$.fn.closest_descendent = function(filter) {
    var found = [];

    //go through every matched element that is a child of the target element
    $(this).find(filter).each(function(){
        //when a match is found, add it to the list
        found.push($(this));
    });

    return found[0]; // Return first match in the list
}
Daniel Tonon
fonte
Para mim, isso parece exatamente .find(filter).first(), apenas mais lento;)
Matthias Samsel 31/03/19
Sim, você está certo. Escrevi isso quando ainda era bastante novo em codificação. Eu acho que eu vou apagar esta resposta
Daniel Tonon
0

Eu usaria o seguinte para incluir o próprio destino se corresponder ao seletor:

    var jTarget = $("#doo");
    var sel = '.pou';
    var jDom = jTarget.find(sel).addBack(sel).first();

Marcação:

<div id="doo" class="pou">
    poo
    <div class="foo">foo</div>
    <div class="pou">pooo</div>
</div>
ling
fonte
0

Mesmo que seja um tópico antigo, não pude resistir à implementação do mais próximo da minha criança. Entrega o primeiro descendente encontrado com menos deslocamento (respiração primeiro). Um é recursivo (favorito pessoal), outro usa uma lista de tarefas, portanto sem recursão como extensões do jQquery.

Espero que alguém se beneficie.

Nota: O recursivo obtém um estouro de pilha e eu melhorei o outro, agora semelhante à resposta anterior fornecida.

jQuery.fn.extend( {

    closestChild_err : function( selector ) { // recursive, stack overflow when not found
        var found = this.children( selector ).first();
        if ( found.length == 0 ) {
            found = this.children().closestChild( selector ).first(); // check all children
        }
        return found;
    },

    closestChild : function( selector ) {
        var todo = this.children(); // start whith children, excluding this
        while ( todo.length > 0 ) {
            var found = todo.filter( selector );
            if ( found.length > 0 ) { // found closest: happy
                return found.first();
            } else {
                todo = todo.children();
            }
        }
        return $();
    },

});  
Eelco
fonte
0

O seguinte plugin retorna os enésimos enésimos descendentes mais próximos.

$.fn.getNthClosestDescendants = function(n, type) {
  var closestMatches = [];
  var children = this.children();

  recursiveMatch(children);

  function recursiveMatch(children) {
    var matches = children.filter(type);

    if (
      matches.length &&
      closestMatches.length < n
    ) {
      var neededMatches = n - closestMatches.length;
      var matchesToAdd = matches.slice(0, neededMatches);
      matchesToAdd.each(function() {
        closestMatches.push(this);
      });
    }

    if (closestMatches.length < n) {
      var newChildren = children.children();
      recursiveMatch(newChildren);
    }
  }

  return closestMatches;
};
Cole
fonte
0

Eu estava procurando por uma solução semelhante (queria todos os descendentes mais próximos, ou seja, a amplitude primeiro + todas as correspondências, independentemente do nível em que ela existe), eis o que acabei fazendo:

var item = $('#find-my-closest-descendant');
item.find(".matching-descendant").filter(function () {
    var $this = $(this);
    return $this.parent().closest("#find-my-closest-descendant").is(item);
}).each(function () {
    // Do what you want here
});

Eu espero que isso ajude.

Sharique Abdullah
fonte
0

Você pode simplesmente colocar,

$("#find-my-closest-descendant").siblings('.closest:first');
Jyoti watpade
fonte