Podobny do jQuery .closest (), ale przechodzący przez potomków?


124

Czy istnieje funkcja podobna do jQuery .closest()przemierzania potomków i zwracania tylko najbliższych?

Wiem, że jest .find()funkcja, ale zwraca wszystkie możliwe dopasowania, a nie najbliższe.

Edytować:

Oto definicja najbliższego (przynajmniej dla mnie) :

W pierwszej kolejności przechodzą wszystkie dzieci, następnie przechodzą poszczególne dzieci.

W podanym poniżej przykładzie id='2'są najbliżsi .closestpotomkowieid="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>

Proszę zobaczyć link JSfiddle .


4
O co chodzi .find("filter here").eq(0)?
— Shadow Wizard is Ear For You

@ShadowWizard cóż, jeśli wykonuje przemierzanie najpierw w głąb, to zerowy element na liście wyników może nie być „najbliższy”, w zależności od tego, co oznacza „najbliższy”.
— Pointy

@Pointy, czy to nie to samo co :firstfiltr?
— Shadow Wizard is Ear For You

Nie, nie sądzę - problem polega na tym, że „: first” i „.eq (0)” odnoszą się do kolejności DOM, ale jeśli „najbliższy” oznacza „najmniej węzłów DOM od siebie”, to „wnuk” pierwsze dziecko zostanie zwrócone zamiast późniejszego „dziecka”, które pasuje do selektora. Nie jestem w 100% pewien, o co chodzi oczywiście w OP.
— Pointy

1
Jest wtyczka jQuery, która dokładnie to robi: plugins.jquery.com/closestDescendant
— TLindig

Odpowiedzi:


44

Zgodnie z twoją definicją closestnapisałem następującą wtyczkę:

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

3
W przeciwieństwie do .closest()funkcji jQuery , to pasuje również do elementu, do którego została wywołana. Zobacz mój jsfiddle . Zmiana na $currentSet = this.children(); // Current placeto zacznie się od jego dzieci zamiast jsfiddle
— allicarn

21
Najbliższa () JQuery może być również dopasowana do bieżącego elementu.
— Dávid Horváth

121

Jeśli mówiąc „najbliższy” potomek masz na myśli pierwsze dziecko, możesz:

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

Lub:

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

Lub, aby wyszukać pierwsze wystąpienie określonego elementu, możesz zrobić:

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

Lub:

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

Jednak naprawdę potrzebujemy solidnej definicji tego, co oznacza „najbliższy potomek”.

Na przykład

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

Który <span>byłby $('#foo').closestDescendent('span')powrót?


1
dzięki @ 999 za szczegółową odpowiedź, moje oczekiwanie na zmarłego jest takie, że najpierw przechodzą wszystkie dzieci, a potem dzieci. Na przykład, który podałeś, drugi zakres zostanie zwrócony
— mamu

8
Więc najpierw oddech, nie głębia
— jessehouwing

1
Doskonała odpowiedź. Chciałbym wiedzieć, czy $('#foo').find('.whatever').first();jest to „najpierw wszerz”, czy „najpierw w głąb”
— Colin

1
Jedynym problemem związanym z tym rozwiązaniem jest to, że jQuery znajdzie WSZYSTKIE pasujące kryteria, a następnie zwróci pierwsze. Chociaż silnik Sizzle jest szybki, oznacza to niepotrzebne dodatkowe wyszukiwanie. Nie ma możliwości skrócenia wyszukiwania i zatrzymania po znalezieniu pierwszego dopasowania.
— icfantv

Wszystkie te przykłady wybierają potomków w pierwszej kolejności w głąb, a nie wszerz, więc nie można ich użyć do rozwiązania pierwotnego pytania. W moich testach wszyscy zwrócili pierwszy zakres, a nie drugi.
— JrBaconCheez

18

Możesz użyć findz :firstselektorem:

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

Powyższa linia znajdzie pierwszy <p>element w potomkach #parent.


2
Myślę, że właśnie tego chciał OP, bez wtyczki. Spowoduje to wybranie pierwszego pasującego elementu w potomkach wybranego elementu dla każdego wybranego elementu. Więc jeśli masz 4 dopasowania z pierwotnym wyborem, możesz skończyć z 4 dopasowaniami, używając find('whatever:first').
— RustyToms

3

A co z tym podejściem?

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

Ten selektor „bezpośredniego dziecka” działa dla mnie.


OP konkretnie prosi o coś, co przechodzi przez potomków, czego ta odpowiedź nie zawiera. To może być dobra odpowiedź, ale nie w przypadku tego konkretnego pytania.
— jumps4fun

@KjetilNordin Może dlatego, że pytanie zostało zmienione od czasu mojej odpowiedzi. Co więcej, definicja najbliższego potomka wciąż nie jest tak jasna, przynajmniej dla mnie.
— klawipo

Zdecydowanie się zgadzam. najbliższy potomek może oznaczać tak wiele rzeczy. Łatwiej z rodzicem w tym przypadku, gdzie zawsze istnieje jeden rodzic dla elementów, na każdym poziomie. Masz również rację co do możliwych zmian. Widzę teraz, że mój komentarz nie był w żaden sposób pomocny, zwłaszcza że było już oświadczenie tego samego typu.
— jumps4fun

1

Czysty roztwór JS (przy użyciu 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;
}

Przykład

Biorąc pod uwagę następującą strukturę:

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;


To rozwiązanie działa, ale opublikowałem tutaj alternatywne rozwiązanie ES6 ( stackoverflow.com/a/55144581/2441655 ), ponieważ nie lubię: 1) whileWyrażenie mające skutki uboczne. 2) Użycie .matchesczeku w dwóch wierszach zamiast tylko jednego.
— Venryx

1

W przypadku, gdy ktoś szuka czystego rozwiązania JS (używając ES6 zamiast jQuery), oto ten, którego używam:

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

Hej, dziękuję za zwrócenie mojej uwagi na Twoją odpowiedź! Masz rację, patrząc na własne, mam wrażenie, że próbuję być zbyt sprytny i faktycznie niezręczny ( matchesdwukrotne bieganie też trochę mnie irytuje). +1 dla Ciebie :)
— ccjmne

0

Myślę, że najpierw musisz zdefiniować, co oznacza „najbliższy”. Jeśli masz na myśli węzeł podrzędny spełniający Twoje kryteria, czyli najkrótszą odległość pod względem linków rodzicielskich, użycie „: first” lub „.eq (0)” niekoniecznie zadziała:

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

W tym przykładzie drugi <span>element „.target” jest „bliżej” „początku” <div>, ponieważ dzieli go tylko jeden przeskok rodzicielski. Jeśli to właśnie masz na myśli mówiąc „najbliżej”, musisz znaleźć minimalną odległość w funkcji filtra. Lista wyników z selektora jQuery jest zawsze w kolejności DOM.

Może:

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

To coś w rodzaju wtyczki hakerskiej ze względu na sposób, w jaki buduje obiekt wynikowy, ale chodzi o to, że musisz znaleźć najkrótszą ścieżkę rodzicielską ze wszystkich pasujących elementów, które znajdziesz. Ten kod kieruje się w stronę elementów w lewo w drzewie DOM z powodu <sprawdzania; <=odchyliłby się w prawo.


0

Ugotowałem to, nie ma jeszcze implementacji dla selektorów pozycyjnych (potrzebują czegoś więcej niż tylko matchesSelector):

Demo: 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);

Prawdopodobnie jest jednak kilka błędów, nie testowałem tego zbytnio.


0

Odpowiedź Roba W. nie do końca mi odpowiadała. Dostosowałem to do tego, co zadziałało.

//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
}

Dla mnie wygląda to dokładnie tak .find(filter).first(), tylko wolniej;)
— Matthias Samsel

Tak, masz rację. Napisałem to, gdy byłem jeszcze całkiem nowy w programowaniu. Myślę, że usunę tę odpowiedź
— Daniel Tonon

0

Użyłbym poniższego, aby dołączyć cel, jeśli pasuje do selektora:

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

Narzut:

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

0

Mimo, że jest to stary temat, nie mogłem się oprzeć realizacji mojego najbliższego Dziecka. Dostarcza pierwszego znalezionego potomka z najmniejszą podróżą (najpierw oddech). Jedna jest rekurencyjna (osobista ulubiona), druga używa listy rzeczy do zrobienia, więc bez rekurencji jako rozszerzenia jQquery.

Mam nadzieję, że ktoś skorzysta.

Uwaga: rekurencyjny dostaje przepełnienie stosu, a ja poprawiłem drugą, teraz podobną do poprzedniej podanej odpowiedzi.

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 $();
    },

});  

0

Następująca wtyczka zwraca n-tego najbliższego potomka.

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

0

Szukałem podobnego rozwiązania (chciałem wszystkich najbliższych potomków, czyli najpierw szerokość + wszystkie dopasowania niezależnie od tego, na jakim poziomie istnieje), oto co zrobiłem:

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

Mam nadzieję, że to pomoże.


0

Możesz po prostu powiedzieć,

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


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.