Similar a jQuery .closest () pero atravesando descendientes?


123

¿Existe una función similar jQuery .closest()pero para atravesar descendientes y devolver solo los más cercanos?

Sé que hay una .find()función, pero devuelve todas las coincidencias posibles, no las más cercanas.

Editar:

Aquí está la definición de más cercano (al menos para mí) :

En primer lugar, se atraviesan todos los niños, luego se atraviesan los niños de cada individuo.

En el ejemplo que se muestra a continuación id='2'se muestran los .closestdescendientes más cercanos 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>

Por favor vea el enlace JSfiddle .


44
¿Qué hay de .find("filter here").eq(0)?
Shadow Wizard es Ear For You

@ShadowWizard bien si hace un recorrido de profundidad primero, entonces el elemento cero en la lista de resultados podría no ser el "más cercano", dependiendo de lo que significa "más cercano".
Puntiagudo

@Pointy, ¿no es lo mismo que :firstfiltro?
Shadow Wizard es Ear For You

No, no lo creo, el problema es que ": primero" y ".eq (0)" se refieren al orden DOM, pero si "más cercano" significa "la menor cantidad de nodos DOM", entonces un "nieto" del el primer hijo se devolvería en lugar de un "hijo" posterior que coincida con el selector. Por supuesto, no estoy 100% seguro de que se trata el OP.
Puntiagudo

1
Hay un complemento jQuery que hace exactamente eso por usted: plugins.jquery.com/closestDescendant
TLindig

Respuestas:


44

Según su definición de closest, he escrito el siguiente complemento:

(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 diferencia de la .closest()función jQuery , también coincide con el elemento en el que se invocó. Mira mi jsfiddle . Al cambiarlo a $currentSet = this.children(); // Current place, comenzará con sus hijos en su
allicarn

21
El más cercano () de JQuery también puede coincidir con el elemento actual.
Dávid Horváth

120

Si por descendiente "más cercano" te refieres al primer hijo, entonces puedes hacer:

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

O:

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

O, para buscar la primera aparición de un elemento específico, puede hacer:

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

O:

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

Realmente, sin embargo, necesitamos una definición sólida de lo que significa "descendiente más cercano".

P.ej

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

¿Qué <span>sería $('#foo').closestDescendent('span')volver?


1
gracias @ 999 por la respuesta detallada, mi expectativa de difunto es que primero se atraviesen todos los niños, luego cada niño individual. En el ejemplo que usted dio, el segundo período sería devuelto
mamu

8
Así que primero respira, no profundiza primero
jessehouwing

1
Excelente respuesta Me interesaría saber si $('#foo').find('.whatever').first();es "primero en amplitud" o "primero en profundidad"
Colin

1
El único problema con esta solución es que jQuery encontrará TODOS los criterios coincidentes y luego devolverá el primero. Si bien el motor Sizzle es rápido, esto representa una búsqueda adicional innecesaria. No hay forma de cortocircuitar la búsqueda y detenerse después de encontrar la primera coincidencia.
icfantv

Todos estos ejemplos seleccionan descendientes de profundidad primero, no de amplitud, por lo que no se pueden usar para resolver la pregunta original. En mis pruebas, todos me devolvieron el primer lapso, no el segundo.
JrBaconCheez

18

Puedes usar findcon el :firstselector:

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

La línea anterior encontrará el primer <p>elemento en los descendientes de #parent.


2
Creo que esto es lo que quería el OP, no se requiere ningún complemento. Esto seleccionará el primer elemento coincidente en los descendientes del elemento seleccionado, para cada elemento seleccionado. Entonces, si tiene 4 coincidencias con su selección original, podría terminar con 4 coincidencias utilizando el find('whatever:first').
RustyToms

3

¿Qué hay de este enfoque?

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

Este selector de "hijo directo" funciona para mí.


El OP pide específicamente algo que atraviese a los descendientes, lo que esta respuesta no hace. Esta podría ser una buena respuesta, pero no para esta pregunta en particular.
jumps4fun

@KjetilNordin Tal vez sea porque la pregunta fue editada desde mi respuesta. Además, la definición de descendiente más cercano todavía no está tan clara, al menos para mí.
klawipo

Definitivamente estoy de acuerdo. descendiente más cercano podría significar muchas cosas. Más fácil con el padre en este caso, donde siempre hay un solo padre para los elementos, en cada nivel. Y también tienes razón sobre posibles ediciones. Ahora veo que mi comentario no fue útil de ninguna manera, especialmente porque ya había una declaración del mismo tipo.
jumps4fun

1

Solución JS pura (usando 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;
}

Ejemplo

Considerando la siguiente estructura:

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;


Esta solución funciona, pero he publicado una solución ES6 alternativa aquí ( stackoverflow.com/a/55144581/2441655 ) ya que no me gusta: 1) La whileexpresión tiene efectos secundarios. 2) Uso del .matchescheque en dos líneas en lugar de solo una.
Venryx

1

En caso de que alguien esté buscando una solución JS pura (usando ES6 en lugar de jQuery), aquí está la que yo 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;
};

¡Hola, gracias por llamar tu atención! Tienes razón, mirando hacia atrás, siento que está tratando de ser demasiado inteligente y realmente incómodo (correr matchesdos veces también me molesta bastante). +1 para ti :)
ccjmne

0

Creo que primero hay que definir qué significa "más cercano". Si te refieres al nodo descendiente que coincide con tus criterios que está a la distancia más corta en términos de enlaces parentales, entonces usar ": first" o ".eq (0)" no necesariamente funcionará:

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

En ese ejemplo, el segundo <span>elemento ".target" está "más cerca" del "inicio" <div>, porque está a solo un salto de los padres. Si eso es lo que quiere decir con "más cercano", necesitaría encontrar la distancia mínima en una función de filtro. La lista de resultados de un selector jQuery siempre está en orden DOM.

Tal vez:

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

Es una especie de complemento de pirateo debido a la forma en que construye el objeto resultante, pero la idea es que tienes que encontrar la ruta parental más corta de todos los elementos coincidentes que encuentres. Este código se inclina hacia los elementos hacia la izquierda en el árbol DOM debido a la <verificación; <=sesgaría hacia la derecha.


0

He preparado esto, no hay implementación para los selectores posicionales (necesitan más que solo matchesSelector) todavía:

Demostración: 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);

Probablemente hay algunos errores, sin embargo, no lo probé mucho.


0

La respuesta de Rob W no funcionó del todo para mí. Lo adapté a esto que funcionó.

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

Para mí esto se ve exactamente igual .find(filter).first(), pero más lento;)
Matthias Samsel

Sí tienes razón. Escribí esto cuando todavía era bastante nuevo en la codificación. Creo que eliminaré esta respuesta
Daniel Tonon

0

Usaría lo siguiente para incluir el objetivo en sí mismo si coincide con el selector:

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

Margen:

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

0

Aunque es un tema antiguo, no pude resistirme a implementar mi niño más cercano. Entrega el primer descendiente encontrado con menos viajes (primero la respiración). Uno es recursivo (favorito personal), otro usa una lista de tareas pendientes sin recurrencia como extensiones jQquery.

Espero que alguien se beneficie.

Nota: El recursivo obtiene desbordamiento de pila, y mejoré el otro, ahora similar a la respuesta anterior dada.

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

El siguiente complemento devuelve los enésimos descendientes más cercanos.

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

Estaba buscando una solución similar (quería todos los descendientes más cercanos, es decir, amplitud primero + todas las coincidencias independientemente del nivel que exista), esto es lo que terminé haciendo:

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

Espero que esto ayude.


0

Simplemente puedes poner,

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


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.