Simile a jQuery .closest () ma attraversando i discendenti?


123

Esiste una funzione simile a jQuery .closest()ma per attraversare i discendenti e restituire solo quelli più vicini?

So che esiste una .find()funzione, ma restituisce tutte le corrispondenze possibili, non le più vicine.

Modificare:

Ecco la definizione di più vicino (almeno per me) :

In primo luogo vengono attraversati tutti i bambini, quindi vengono attraversati i bambini di ogni individuo.

Nell'esempio fornito di seguito id='2'sono i .closestdiscendenti più vicini diid="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>

Si prega di consultare il collegamento JSfiddle .


4
Di cosa .find("filter here").eq(0)?
Shadow Wizard is Ear For You

@ShadowWizard bene se esegue un attraversamento in profondità, l'elemento zero nell'elenco dei risultati potrebbe non essere il "più vicino", a seconda di cosa significa "più vicino".
Pointy

@Pointy non è lo stesso del :firstfiltro?
Shadow Wizard is Ear For You

No, non la penso così - il problema è che ": first" e ".eq (0)" si riferiscono all'ordine DOM, ma se "più vicino" significa "meno nodi DOM lontani", allora un "nipote" del viene restituito il primo figlio invece di un "figlio" successivo che corrisponde al selettore. Ovviamente non sono sicuro al 100% di questo.
Pointy

1
C'è un plug-in jQuery che fa esattamente questo per te: plugins.jquery.com/closestDescendant
TLindig

Risposte:


44

Secondo la tua definizione di closest, ho scritto il seguente 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);

3
A differenza della .closest()funzione jQuery , questa corrisponde anche all'elemento su cui è stata chiamata. Vedi il mio jsfiddle . Modificandolo $currentSet = this.children(); // Current placeinizierà con i suoi bambini invece jsfiddle
allicarn

21
La più vicina () di JQuery può anche essere abbinata all'elemento corrente.
Dávid Horváth

120

Se per discendente "più prossimo" intendi il primo figlio, puoi fare:

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

O:

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

Oppure, per cercare la prima occorrenza di un elemento specifico, potresti fare:

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

O:

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

In realtà, però, abbiamo bisogno di una definizione solida di cosa significhi "discendente più prossimo".

Per esempio

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

Quale <span>sarebbe $('#foo').closestDescendent('span')tornare?


1
grazie @ 999 per la risposta dettagliata, la mia aspettativa di deceduto è che prima vengono attraversati tutti i bambini, quindi ogni individuo è bambino. Nell'esempio che hai fornito, verrebbe restituito il secondo intervallo
mamu

8
Quindi il primo respiro, non il primo in profondità
jessehouwing

1
Ottima risposta. Sarei interessato a sapere se $('#foo').find('.whatever').first();è "prima larghezza" o "prima profondità"
Colin

1
L'unico problema con questa soluzione è che jQuery troverà TUTTI i criteri di corrispondenza e quindi restituirà il primo. Sebbene il motore Sizzle sia veloce, ciò rappresenta una ricerca extra non necessaria. Non c'è modo di cortocircuitare la ricerca e interromperla dopo aver trovato la prima corrispondenza.
icfantv

Questi esempi selezionano tutti i discendenti in profondità, non in larghezza, quindi non possono essere utilizzati per risolvere la domanda originale. Nella mia prova hanno restituito tutti la prima campata, non la seconda.
JrBaconCheez

18

Puoi usare findcon il :firstselettore:

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

La riga sopra troverà il primo <p>elemento nei discendenti di #parent.


2
Penso che questo sia ciò che voleva l'OP, nessun plugin richiesto. Questo selezionerà il primo elemento corrispondente nei discendenti dell'elemento selezionato, per ogni elemento selezionato. Quindi, se hai 4 corrispondenze con la tua selezione originale, potresti finire con 4 corrispondenze usando il find('whatever:first').
RustyToms,

3

E questo approccio?

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

Questo selettore "figlio diretto" funziona per me.


l'OP chiede specificamente qualcosa che attraversi i discendenti, cosa che questa risposta non fa. Questa potrebbe essere una buona risposta, ma non per questa particolare domanda.
jumps4fun

@KjetilNordin Forse è perché la domanda è stata modificata dopo la mia risposta. Inoltre, la definizione di discendente più vicino non è ancora così chiara, almeno per me.
klawipo

Sono decisamente d'accordo. discendente più vicino potrebbe significare tante cose. Più facile con il genitore in questo caso, dove c'è sempre un solo genitore per gli elementi, a ogni livello. E hai ragione anche sulle possibili modifiche. Vedo ora che il mio commento non è stato d'aiuto in alcun modo, soprattutto perché c'era già un'affermazione dello stesso tipo.
jumps4fun il

1

Soluzione JS pura (utilizzando 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;
}

Esempio

Considerando la seguente struttura:

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;


Questa soluzione funziona, ma ho pubblicato una soluzione ES6 alternativa qui ( stackoverflow.com/a/55144581/2441655 ) poiché non mi piace: 1) L' whileespressione che ha effetti collaterali. 2) Utilizzo del .matchessegno di spunta su due righe anziché una sola.
Venryx

1

Nel caso qualcuno stia cercando una soluzione JS pura (utilizzando ES6 invece di jQuery), ecco quella che 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;
};

Ehi, grazie per aver portato la tua risposta alla mia attenzione! Hai ragione, ripensando al mio, sembra che stia cercando di essere troppo intelligente e in realtà imbarazzante (correre matchesdue volte mi infastidisce anche un po '). +1 per te :)
ccjmne

0

Penso che prima devi definire cosa significa "più vicino". Se intendi il nodo discendente che corrisponde ai tuoi criteri che è la distanza più breve in termini di collegamenti parentali, l'utilizzo di ": first" o ".eq (0)" non funzionerà necessariamente:

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

In questo esempio, il secondo <span>elemento ".target" è "più vicino" all '"inizio" <div>, perché è a un solo salto dei genitori. Se questo è ciò che intendi per "più vicino", dovresti trovare la distanza minima in una funzione di filtro. L'elenco dei risultati da un selettore jQuery è sempre in ordine DOM.

Può essere:

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

È una specie di plug-in di hacking a causa del modo in cui costruisce l'oggetto risultato, ma l'idea è che devi trovare il percorso parentale più breve tra tutti gli elementi corrispondenti che trovi. Questo codice è orientato verso gli elementi a sinistra dell'albero DOM a causa del <controllo; <=sarebbe orientato verso destra.


0

Ho inventato questo, nessuna implementazione per i selettori di posizione (hanno bisogno di più di un semplice matchesSelector) ancora:

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

Probabilmente ci sono alcuni bug però, non l'ho testato molto.


0

La risposta di Rob W non ha funzionato per me. L'ho adattato a questo che ha funzionato.

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

Per me questo sembra esattamente come .find(filter).first(), solo più lento;)
Matthias Samsel

Sì, hai ragione. L'ho scritto quando ero ancora abbastanza nuovo nella programmazione. Penso che cancellerò questa risposta
Daniel Tonon

0

Vorrei utilizzare quanto segue per includere l'obiettivo stesso se corrisponde al selettore:

    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>

0

Anche se è un vecchio argomento non ho potuto resistere alla realizzazione del mio figlio più vicino. Fornisce il primo discendente trovato con il minor viaggio (prima il respiro). Uno è ricorsivo (preferito personale), altri usano una lista di cose da fare quindi senza ricorsione come estensioni jQquery.

Spero che qualcuno ne tragga beneficio.

Nota: il ricorsivo ottiene lo stack overflow e ho migliorato l'altro, ora simile alla risposta precedente fornita.

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

Il seguente plugin restituisce l'ennesimo discendente più vicino.

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

Stavo cercando una soluzione simile (volevo tutti i discendenti più vicini, cioè l'ampiezza prima + tutte le corrispondenze indipendentemente dal livello che esiste), ecco cosa ho finito per fare:

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

Spero che aiuti.


0

Puoi semplicemente mettere,

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


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.