Mirip dengan jQuery .closest () tapi traversing descendants?

123

Apakah ada fungsi yang mirip dengan jQuery .closest()tetapi untuk melintasi turunan dan hanya mengembalikan yang terdekat?

Saya tahu bahwa ada .find()fungsi, tetapi mengembalikan semua kemungkinan kecocokan, bukan yang terdekat.

Edit:

Berikut definisi terdekat (setidaknya bagi saya) :

Pertama-tama, semua anak dilintasi, kemudian setiap individu anak-anak dilintasi.

Dalam contoh yang diberikan di bawah id='2'ini adalah .closestketurunan terdekat dariid="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>

Silakan lihat tautan JSfiddle .

mamu
sumber
4
Tentang apa .find("filter here").eq(0)?
Shadow Wizard adalah Ear For You
@ShadowWizard baik jika ia melakukan traversal depth-first, maka elemen ke-nol dalam daftar hasil mungkin bukan yang "terdekat", bergantung pada arti "terdekat".
Pointy
@ Pointy bukankah itu sama dengan :firstfilter?
Shadow Wizard adalah Ear For You
Tidak, menurut saya tidak - masalahnya adalah bahwa ": pertama" dan ".eq (0)" merujuk ke urutan DOM, tetapi jika "terdekat" berarti "simpul DOM paling sedikit" maka "cucu" dari anak pertama akan dikembalikan, bukan "anak" nanti yang cocok dengan pemilih. Saya tidak 100% yakin itu tentang OP tentu saja.
Pointy
1
Ada plugin jQuery yang sebenarnya melakukan itu untuk Anda: plugins.jquery.com/closestDescendant
TLindig

Jawaban:

44

Menurut definisi Anda closest, saya telah menulis plugin berikut:

(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.
sumber
3
Berbeda dengan .closest()fungsi jQuery , ini cocok dengan elemen yang dipanggilnya juga. Lihat jsfiddle saya . Dengan mengubahnya menjadi $currentSet = this.children(); // Current placeitu akan dimulai dengan itu anak-anak bukan jsfiddle
allicarn
21
Terdekat () JQuery juga bisa dicocokkan dengan elemen saat ini.
Dávid Horváth
120

Jika yang dimaksud dengan keturunan "terdekat" yang Anda maksud adalah anak pertama maka Anda dapat melakukan:

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

Atau:

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

Atau, untuk mencari kemunculan pertama elemen tertentu, Anda bisa melakukan:

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

Atau:

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

Sungguh, kita membutuhkan definisi yang kuat tentang apa artinya "keturunan terdekat".

Misalnya

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

Yang mana yang <span>akan $('#foo').closestDescendent('span')kembali?

James
sumber
1
terima kasih @ 999 untuk jawaban rinci, harapan saya kepada almarhum adalah pertama semua anak dilintasi, kemudian setiap individu anak-anak. Dalam contoh yang Anda berikan, rentang kedua akan dikembalikan
mamu
8
Jadi bernafas dulu, bukan yang pertama
jessehouwing
1
Jawaban yang sangat bagus. Saya tertarik untuk mengetahui apakah $('#foo').find('.whatever').first();itu "breadth-first" atau "depth-first"
Colin
1
Satu masalah dengan solusi ini adalah jQuery akan menemukan SEMUA kriteria yang cocok dan kemudian mengembalikan yang pertama. Meskipun mesin Sizzle cepat, ini menunjukkan pencarian ekstra yang tidak perlu. Tidak ada cara untuk mempersingkat pencarian dan berhenti setelah kecocokan pertama ditemukan.
icfantv
Semua contoh ini memilih turunan yang mengutamakan kedalaman, bukan yang pertama lebar, jadi ini tidak dapat digunakan untuk menjawab pertanyaan asli. Dalam pengujian saya, mereka semua mengembalikan rentang pertama, bukan yang kedua.
JrBaconCheez
18

Anda dapat menggunakan finddengan :firstselektor:

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

Baris di atas akan menemukan <p>elemen pertama pada turunan #parent.

IkanBasketGordo
sumber
2
Saya pikir ini yang diinginkan OP, tidak perlu plugin. Ini akan memilih elemen pertama yang cocok di turunan elemen yang dipilih, untuk setiap elemen yang dipilih. Jadi jika Anda memiliki 4 kecocokan dengan pilihan awal Anda, Anda bisa berakhir dengan 4 kecocokan menggunakan find('whatever:first').
RustyToms
3

Bagaimana dengan pendekatan ini?

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

Pemilih "anak langsung" ini bekerja untuk saya.

klawipo
sumber
OP secara khusus meminta sesuatu yang melintasi keturunan, yang jawabannya tidak. Ini mungkin jawaban yang bagus, tetapi tidak untuk pertanyaan khusus ini.
jumps4fun
@KjetilNordin Mungkin itu karena pertanyaannya sudah diedit sejak jawaban saya. Apalagi definisi keturunan terdekat masih belum begitu jelas, setidaknya bagi saya.
klawipo
Saya sangat setuju. keturunan terdekat bisa berarti banyak hal. Lebih mudah dengan induk dalam hal ini, di mana selalu ada satu orang tua untuk elemen, di setiap tingkat. Dan Anda juga benar tentang kemungkinan pengeditan. Saya melihat sekarang bahwa komentar saya tidak membantu dengan cara apa pun, terutama karena sudah ada pernyataan dengan jenis yang sama.
jumps4fun
1

Solusi JS murni (menggunakan 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;
}

Contoh

Mempertimbangkan struktur berikut:

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.dll
sumber
Solusi ini berfungsi, tetapi saya telah memposting solusi ES6 alternatif di sini ( stackoverflow.com/a/55144581/2441655 ) karena saya tidak suka: 1) whileEkspresi memiliki efek samping. 2) Penggunaan tanda .matchescentang pada dua baris, bukan hanya satu.
Venryx
1

Jika seseorang mencari solusi JS murni (menggunakan ES6, bukan jQuery), inilah yang saya gunakan:

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
sumber
Hei, terima kasih telah menyampaikan jawaban Anda kepada saya! Anda benar, melihat ke belakang pada saya sendiri, rasanya seperti mencoba menjadi terlalu pintar dan benar-benar canggung (berlari matchesdua kali juga cukup mengganggu saya). +1 untuk Anda :)
ccjmne
0

Saya pikir pertama-tama Anda harus mendefinisikan apa artinya "paling dekat". Jika yang Anda maksud adalah node turunan yang cocok dengan kriteria Anda yang merupakan jarak terpendek dalam istilah tautan orang tua, maka menggunakan ": first" atau ".eq (0)" tidak selalu berfungsi:

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

Dalam contoh itu, <span>elemen ".target" kedua adalah "lebih dekat" ke "awal" <div>, karena hanya berjarak satu lompatan orang tua. Jika itu yang Anda maksud dengan "terdekat", Anda perlu mencari jarak minimum dalam fungsi filter. Daftar hasil dari pemilih jQuery selalu dalam urutan DOM.

Mungkin:

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

Itu semacam plugin hack karena cara membangun objek hasil, tetapi idenya adalah Anda harus menemukan jalur parental terpendek dari semua elemen yang cocok yang Anda temukan. Kode ini bias ke arah elemen ke kiri di pohon DOM karena <pemeriksaan; <=akan bias ke kanan.

Runcing
sumber
0

Saya memasak ini, belum ada implementasi untuk pemilih posisi (mereka membutuhkan lebih dari sekadar 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);

Mungkin ada beberapa bug, tidak terlalu banyak mengujinya.

Esailija
sumber
0

Jawaban Rob W tidak cukup berhasil untuk saya. Saya menyesuaikannya dengan ini yang berhasil.

//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
sumber
Bagi saya ini terlihat persis seperti .find(filter).first(), hanya lebih lambat;)
Matthias Samsel
Ya Anda benar. Saya menulis ini ketika saya masih cukup baru dalam pengkodean. Saya rasa saya akan menghapus jawaban ini
Daniel Tonon
0

Saya akan menggunakan yang berikut ini untuk menyertakan target itu sendiri jika cocok dengan selektor:

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

Markup:

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

Meskipun ini adalah topik lama, saya tidak bisa menahan diri untuk menerapkan anak-anak terdekat saya. Menghadirkan keturunan pertama yang ditemukan dengan perjalanan paling sedikit (nafas lebih dulu). Salah satunya adalah rekursif (favorit pribadi), yang lain menggunakan daftar yang harus dilakukan sehingga tanpa rekursi sebagai ekstensi jQquery.

Semoga seseorang mendapat manfaat.

Catatan: Rekursif mendapat stack overflow, dan saya meningkatkan yang lain, sekarang simular dengan jawaban sebelumnya yang diberikan.

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
sumber
0

Plugin berikut mengembalikan keturunan terdekat ke-n.

$.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
sumber
0

Saya sedang mencari solusi serupa (saya ingin semua keturunan terdekat, yaitu luasnya pertama + semua kecocokan terlepas dari level mana itu ada), inilah yang akhirnya saya lakukan:

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

Saya harap ini membantu.

Sharique Abdullah
sumber
0

Sederhananya,

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