Similaire à jQuery .closest () mais traversant les descendants?

123

Existe-t-il une fonction similaire à jQuery .closest()mais pour parcourir les descendants et ne renvoyer que les plus proches?

Je sais qu'il y a une .find()fonction, mais elle renvoie toutes les correspondances possibles, pas les plus proches.

Éditer:

Voici la définition du plus proche (du moins pour moi) :

En premier lieu, tous les enfants sont traversés, puis chaque enfant individuel est traversé.

Dans l'exemple ci id='2'- dessous, les .closestdescendants les plus proches 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>

Veuillez consulter le lien JSfiddle .

mamu
la source
4
Et quoi .find("filter here").eq(0)?
Shadow Wizard est une oreille pour vous le
@ShadowWizard bien s'il effectue une traversée en profondeur d'abord, alors l'élément zéro de la liste de résultats peut ne pas être le "plus proche", selon ce que signifie "le plus proche".
Pointy
@Pointy n'est-ce pas la même chose que le :firstfiltre?
Shadow Wizard est une oreille pour vous le
Non, je ne pense pas - le problème est que ": first" et ".eq (0)" font référence à l'ordre DOM, mais si "le plus proche" signifie "le moins de nœuds DOM", alors un "petit-enfant" du le premier enfant serait retourné au lieu d'un "enfant" ultérieur qui correspond au sélecteur. Je ne suis pas sûr à 100% que ce soit le but du PO, bien sûr.
Pointy
1
Il existe un plugin jQuery qui fait exactement cela pour vous: plugins.jquery.com/closestDescendant
TLindig

Réponses:

44

Selon votre définition de closest, j'ai écrit le plugin suivant:

(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
la source
3
Contrairement à la .closest()fonction jQuery , cela correspond également à l'élément sur lequel elle a été appelée. Voir mon jsfiddle . En le changeant, il $currentSet = this.children(); // Current placecommencera par ses enfants à la place jsfiddle
allicarn
21
Le plus proche () de JQuery peut également être mis en correspondance avec l'élément actuel.
Dávid Horváth
120

Si par descendant "le plus proche" vous entendez le premier enfant, vous pouvez faire:

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

Ou:

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

Ou, pour rechercher la première occurrence d'un élément spécifique, vous pouvez faire:

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

Ou:

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

En réalité, nous avons besoin d'une définition solide de ce que signifie «descendant le plus proche».

Par exemple

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

Ce qui <span>serait de $('#foo').closestDescendent('span')retour?

James
la source
1
merci @ 999 pour une réponse détaillée, mon attente de la personne décédée est d'abord que tous les enfants sont traversés, puis les enfants de chaque individu. Dans l'exemple que vous avez donné, le deuxième intervalle serait renvoyé
mamu
8
Alors souffle d'abord, pas profondeur d'abord
jessehouwing
1
Excellente réponse. Je serais intéressé de savoir si $('#foo').find('.whatever').first();"largeur d'abord" ou "profondeur d'abord"
Colin
1
Le seul problème avec cette solution est que jQuery trouvera TOUS les critères correspondants, puis retournera le premier. Bien que le moteur Sizzle soit rapide, cela représente une recherche supplémentaire inutile. Il n'y a aucun moyen de court-circuiter la recherche et de s'arrêter une fois la première correspondance trouvée.
icfantv
Ces exemples sélectionnent tous des descendants de profondeur d'abord, pas de largeur d'abord, ils ne peuvent donc pas être utilisés pour résoudre la question d'origine. Lors de mes tests, ils ont tous renvoyé la première travée, pas la seconde.
JrBaconCheez
18

Vous pouvez utiliser findavec le :firstsélecteur:

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

La ligne ci-dessus trouvera le premier <p>élément dans les descendants de #parent.

PoissonPanierGordo
la source
2
Je pense que c'est ce que l'OP voulait, aucun plugin requis. Cela sélectionnera le premier élément correspondant dans les descendants de l'élément sélectionné, pour chaque élément sélectionné. Donc, si vous avez 4 correspondances avec votre sélection d'origine, vous pourriez vous retrouver avec 4 correspondances en utilisant le find('whatever:first').
RustyToms
3

Et cette approche?

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

Ce sélecteur "enfant direct" fonctionne pour moi.

klawipo
la source
l'OP demande spécifiquement quelque chose qui traverse les descendants, ce que cette réponse ne fait pas. Cela pourrait être une bonne réponse, mais pas pour cette question particulière.
jumps4fun
@KjetilNordin C'est peut-être parce que la question a été modifiée depuis ma réponse. De plus, la définition du descendant le plus proche n'est toujours pas aussi claire, du moins pour moi.
klawipo
Je suis tout à fait d’accord. le descendant le plus proche pourrait signifier tant de choses. Plus facile avec le parent dans ce cas, où il y a toujours un parent unique aux éléments, à chaque niveau. Et vous avez également raison sur les modifications possibles. Je vois maintenant que mon commentaire n'était d'aucune manière utile, d'autant plus qu'il y avait déjà une déclaration du même type.
jumps4fun
1

Solution Pure JS (utilisant 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;
}

Exemple

Compte tenu de la structure suivante:

div == 0 $
├── div == 1 $
│ ├── div
│ ├── div.findme == 4 $
│ ├── 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
la source
Cette solution fonctionne, mais j'ai posté une solution ES6 alternative ici ( stackoverflow.com/a/55144581/2441655 ) car je n'aime pas: 1) L' whileexpression ayant des effets secondaires. 2) Utilisation du .matcheschèque sur deux lignes au lieu d'une seule.
Venryx
1

Au cas où quelqu'un recherche une solution JS pure (en utilisant ES6 au lieu de jQuery), voici celle que j'utilise:

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
la source
Hé, merci d'avoir porté votre réponse à mon attention! Tu as raison, en regardant le mien, on a l'impression d'essayer d'être trop intelligent et vraiment maladroit (courir matchesdeux fois me dérange aussi un peu). +1 pour vous :)
ccjmne
0

Je pense que vous devez d'abord définir ce que signifie «le plus proche». Si vous voulez dire le nœud descendant correspondant à vos critères qui est la distance la plus courte en termes de liens parentaux, alors utiliser ": first" ou ".eq (0)" ne fonctionnera pas nécessairement:

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

Dans cet exemple, le deuxième <span>élément ".target" est "plus proche" du "début" <div>, car il n'est qu'à un saut parental. Si c'est ce que vous entendez par «le plus proche», vous devez trouver la distance minimale dans une fonction de filtre. La liste de résultats d'un sélecteur jQuery est toujours dans l'ordre DOM.

Peut être:

$.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;
};

C'est une sorte de plugin de piratage en raison de la façon dont il construit l'objet de résultat, mais l'idée est que vous devez trouver le chemin parental le plus court parmi tous les éléments correspondants que vous trouvez. Ce code fait un biais vers les éléments vers la gauche dans l'arborescence DOM à cause de la <vérification; <=serait biaisé à droite.

Pointu
la source
0

J'ai concocté ceci, pas d'implémentation pour les sélecteurs de position (ils ont besoin de plus que juste matchesSelector) pour le moment:

Démo: 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);

Il y a probablement quelques bugs cependant, je ne l'ai pas beaucoup testé.

Esailija
la source
0

La réponse de Rob W n'a pas vraiment fonctionné pour moi. Je l'ai adapté à cela qui a fonctionné.

//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
la source
Pour moi, cela ressemble exactement .find(filter).first(), mais plus lentement;)
Matthias Samsel
Ouais tu as raison. J'ai écrit ceci quand j'étais encore assez nouveau dans le codage. Je pense que je vais supprimer cette réponse
Daniel Tonon
0

J'utiliserais ce qui suit pour inclure la cible elle-même si elle correspond au sélecteur:

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

Balisage:

<div id="doo" class="pou">
    poo
    <div class="foo">foo</div>
    <div class="pou">pooo</div>
</div>
lingue
la source
0

Même si c'est un vieux sujet, je n'ai pas pu résister à l'implémentation de mon enfant le plus proche. Fournit le premier descendant trouvé avec le moins de déplacements (souffle d'abord). L'un est récursif (favori personnel), l'autre utilise une liste de tâches donc sans récursivité comme extensions jQquery.

J'espère que quelqu'un en profitera.

Remarque: le récursif obtient un débordement de pile, et j'ai amélioré l'autre, maintenant similaire à la réponse précédente donnée.

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
la source
0

Le plugin suivant renvoie le nième descendant le plus proche.

$.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
la source
0

Je cherchais une solution similaire (je voulais tous les descendants les plus proches, c'est-à-dire la largeur en premier + toutes les correspondances quel que soit le niveau auquel elle existe), voici ce que j'ai fini par faire:

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
});

J'espère que ça aide.

Sharique Abdullah
la source
0

Vous pouvez simplement dire,

$("#find-my-closest-descendant").siblings('.closest:first');
Watpade Jyoti
la source