Similar to jQuery .closest() but traversing descendants?

Jquery

Jquery Problem Overview


Is there a function similar to jQuery .closest() but for traversing descendants and returning only closest ones?

I know that there is .find() function, but it returns all possible matches, not closest.

Edit:

Here is definition of closest (at least for me):

In first place all childrens are traversed, then each individuals childrens are traversed.

In example given below id='2' is closest .closest descendants of id="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>

Please see JSfiddle link.

Jquery Solutions


Solution 1 - Jquery

If by "closest" descendant you mean the first child then you can do:

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

Or:

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

Or, to look for the first occurrence of a specific element, you could do:

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

Or:

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

Really though, we need a solid definition of what "closest descendant" means.

E.g.

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

Which <span> would $('#foo').closestDescendent('span') return?

Solution 2 - Jquery

According to your definition of closest, I've written the following 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);

Solution 3 - Jquery

You can use find with the :first selector:

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

The above line will find the first <p> element in the descendants of #parent.

Solution 4 - Jquery

What about this approach?

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

This "direct child" selector works for me.

Solution 5 - Jquery

In case someone's looking for a pure JS solution (using ES6 instead of jQuery), here's the one I use:

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

Solution 6 - Jquery

I think that first you have to define what "closest" means. If you mean the descendant node matching your criteria that is the shortest distance away in terms of parental links, then using ":first" or ".eq(0)" won't necessarily work:

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

In that example, the second ".target" <span> element is "closer" to the "start" <div>, because it's only one parental hop away. If that's what you mean by "closest", you'd need to find the minimum distance in a filter function. The result list from a jQuery selector is always in DOM order.

Maybe:

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

That's sort-of a hack plugin because of the way it builds the result object, but the idea is that you've got to find the shortest parental path out of all the matching elements you find. This code biases towards elements leftward in the DOM tree because of the < check; <= would bias rightwards.

Solution 7 - Jquery

I cooked up this, no implementation for positional selectors (they need more than just matchesSelector) yet:

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

There is probably some bugs though, didn't test it very much.

Solution 8 - Jquery

Rob W's answer didn't quite work for me. I adapted it to this which did work.

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

Solution 9 - Jquery

I would use the following to include the target itself if it matches the selector:

    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>

Solution 10 - Jquery

The following plugin returns the nth closest descendants.

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

Solution 11 - Jquery

I was looking for a similar solution (I wanted all closest descendants, i.e. breadth first + all matches regardless of which level it exists), here's what I ended up doing:

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

I hope this helps.

Solution 12 - Jquery

Pure JS solution (using 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;
}

Example

Considering the following structure:

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;

<div id="e0">
    <div id="e1">
        <div></div>
        <div id="e4" class="findme"></div>
        <div></div>
        <div></div>
    </div>
    <div id="e2" class="findme">
        <div></div>
        <div></div>
    </div>
    <div id="e3">
        <div></div>
        <div></div>
        <div></div>
    </div>
</div>

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

const [$0, $1, $2, $3, $4] = [0, 1, 2, 3, 4].map(x => document.querySelector(`#e${x}`));

console.log(closestDescendant($0, '.findme')); // $2
console.log(closestDescendant($1, '.findme')); // $4
console.log(closestDescendant($2, '.findme')); // $2
console.log(closestDescendant($3, '.findme')); // null

Solution 13 - Jquery

There is an excellent article that points out that what the OP requires can be achieved easily with closest [Geeks for Geeks]
1https://www.geeksforgeeks.org/jquery-closest-with-examples/

Solution 14 - Jquery

Even though it's an old topic I could not resist implementing my closestChild. Delivers the first found descendant with least traveling ( breath first ). One is recursive (personal favorite ), other using a todo list so without recursion as jQquery extensions.

Hope someone benefits.

Note : The recursive gets stack overflow, and I improved the other, now simular to previous answer given.

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

Solution 15 - Jquery

You can just simply put,

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

Solution 16 - Jquery

you have a lot of options, however $("#parent").children(".child"); is the fastet. check this article for details and benchmark

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmamuView Question on Stackoverflow
Solution 1 - JqueryJamesView Answer on Stackoverflow
Solution 2 - JqueryRob WView Answer on Stackoverflow
Solution 3 - JqueryFishBasketGordoView Answer on Stackoverflow
Solution 4 - JqueryklawipoView Answer on Stackoverflow
Solution 5 - JqueryVenryxView Answer on Stackoverflow
Solution 6 - JqueryPointyView Answer on Stackoverflow
Solution 7 - JqueryEsailijaView Answer on Stackoverflow
Solution 8 - JqueryDaniel TononView Answer on Stackoverflow
Solution 9 - JquerylingView Answer on Stackoverflow
Solution 10 - JquerycoleView Answer on Stackoverflow
Solution 11 - JquerySharique AbdullahView Answer on Stackoverflow
Solution 12 - JqueryccjmneView Answer on Stackoverflow
Solution 13 - JqueryJean G.TView Answer on Stackoverflow
Solution 14 - JqueryEelcoView Answer on Stackoverflow
Solution 15 - JqueryJyoti watpadeView Answer on Stackoverflow
Solution 16 - JqueryAbou-EmishView Answer on Stackoverflow