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 .

mamu
źródło
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);
Rob W
źródło
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?

James
źródło
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.

FishBasketGordo
źródło
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.

klawipo
źródło
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;

ccjmne
źródło
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;
};
Venryx
źródło
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.

Spiczasty
źródło
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.

Esailija
źródło
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
}
Daniel Tonon
źródło
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>
molwa
źródło
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 $();
    },

});  
Eelco
źródło
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;
};
kapusta
źródło
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.

Sharique Abdullah
źródło
0

Możesz po prostu powiedzieć,

$("#find-my-closest-descendant").siblings('.closest:first');
Jyoti watpade
źródło