2

If I have a set of elements:

<div class="start">
    <div>
        <span>Text 1</span>
    </div>
</div>

<div class="start">
    <span>
        <div>
            <span>Text 2</span>
        </div>
        <div>
            <div>
                <span>Text 3</span>
            </div>
        </div>
    </span>
</div>

What is the best way (using jQuery) to retrieve the most nested child elements (in this case the spans "Text 1" and "Text 3") without knowing what the element structure will be beforehand specifically?

Here's a jsFiddle I'm working with.

Also, I apologize if this has been asked before, but I couldn't find anything like this question specifically.

Matt K
  • 7,207
  • 5
  • 39
  • 60
  • Do a while loop that runs until the length of current.children is 0. However, how do you handle the case where there are, for example multiple divs with a singe span? then you will have multiple most nested child elements. – Kevin B Oct 08 '13 at 21:47
  • possible duplicate of [select deepest child in jQuery](http://stackoverflow.com/questions/3787924/select-deepest-child-in-jquery) – David Thomas Oct 08 '13 at 21:49
  • 1
    @DavidThomas It is the same question but solutions presented here focused on recursivity while there is was by using **while** cycles. – Fabricio Oct 08 '13 at 22:40
  • 1
    You will get more complete answers, if your HTML doesn't have the deepest node as the only node with no children and as the last descendant. Both of those simplifications in your HTML are getting you answers that don't work for the general case. Put some more top level children in there after the deepest node like shown here:http://jsfiddle.net/jfriend00/8tC3a/. – jfriend00 Oct 08 '13 at 23:16
  • This jquery plugin may help: https://github.com/martinille/jquery.deepest.js – Martin Ille Dec 29 '22 at 03:05

5 Answers5

4

Here's an implementation that uses a treeWalk function I had written earlier and then wraps it in a jquery method that finds the deepest descendant of each item in the passed in jQuery object and returns a new jQuery object containing those nodes.

A solution with recursion and lots of jQuery can be done with lots less code, but it will likely be slower. This is based on a generic native JS tree walk function that walks a tree.

Working demo with more complicated HTML test case than the OP's HTML: http://jsfiddle.net/jfriend00/8tC3a/

$.fn.findDeepest = function() {
    var results = [];
    this.each(function() {
        var deepLevel = 0;
        var deepNode = this;
        treeWalkFast(this, function(node, level) {
            if (level > deepLevel) {
                deepLevel = level;
                deepNode = node;
            }
        });
        results.push(deepNode);
    });
    return this.pushStack(results);
};

var treeWalkFast = (function() {
    // create closure for constants
    var skipTags = {"SCRIPT": true, "IFRAME": true, "OBJECT": true, "EMBED": true};
    return function(parent, fn, allNodes) {
        var node = parent.firstChild, nextNode;
        var level = 1;
        while (node && node != parent) {
            if (allNodes || node.nodeType === 1) {
                if (fn(node, level) === false) {
                    return(false);
                }
            }
            // if it's an element &&
            //    has children &&
            //    has a tagname && is not in the skipTags list
            //  then, we can enumerate children
            if (node.nodeType === 1 && node.firstChild && !(node.tagName && skipTags[node.tagName])) {                
                node = node.firstChild;
                ++level;
            } else if (node.nextSibling) {
                node = node.nextSibling;
            } else {
                // no child and no nextsibling
                // find parent that has a nextSibling
                --level;
                while ((node = node.parentNode) != parent) {
                    if (node.nextSibling) {
                        node = node.nextSibling;
                        break;
                    }
                    --level;
                }
            }
        }
    }
})();

var deeps = $(".start").findDeepest();

deeps.each(function(i,v){
    $("#results").append(
        $("<li>").html($(v).prop("tagName") + " " + $(v).html())
    );
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • This is exactly what I was looking for, thank you! I like how fast it works, even though using jQuery methods would make it more simple. – Matt K Oct 09 '13 at 13:30
  • 1
    @MatthewKeefe - yeah, most of the time the extra overhead of jQuery is worth it for the convenience and speed of programming. But for anything that involves a lot of iterations and is speed sensitive, you often want to at least remove the creation of new jQuery objects from the inner loop. Anyway, glad this worked for you. – jfriend00 Oct 09 '13 at 20:22
2

You need to use the :last filter

$('.start').find(':last');

Working fiddle: http://jsfiddle.net/BmEzd/1/

Lepidosteus
  • 11,779
  • 4
  • 39
  • 51
  • 2
    This only finds the last descendant, not the deepest descendant so it won't work if the deepest descendant is not the also the last descendant. It works on the OP's HTML example only because it doesn't cover the general case. – jfriend00 Oct 08 '13 at 22:42
  • It's just a coincidence that this sample works because in your sample the last element is the deepest-nested one. It rarely returns the correct element – Philipp Jun 26 '22 at 14:55
2

Another method(possibly not so fast) with minimal lines of code could be as follows:

var all_matched_elements = $(":contains('" + text_to_search + "')");
var all_parent_elements = $(all_matched_elements).parents();
var all_deepest_matches = $(all_matched_elements).not(all_parent_elements);

This method is especially useful if your text is inside an element that also has other children elements as follows:

<div class="start">
    <div>
        <div>Text 1<span>Alpha Beta Gamma</span></div>
    </div>
</div>

However, the above code would not work for a DOM that looks like following:

<div class="start">
        <div>
            <div id="zzz">search-text
                <div id="aaa">search-text</div>
            </div>  
        </div>
 </div>

In above case, it would only select inner-most with id "#aaa". It would drop id "#zzz". If one wants to also choose with id "#zzz" then code at the start has to be modified to drop elements from "all_parent_elements" whose direct text also matches search text.

Methos
  • 13,608
  • 11
  • 46
  • 49
  • Upvoted because I reused this code to help someone else here: http://stackoverflow.com/questions/40997444/get-specific-object-based-on-text-in-the-object/40997918#40997918 – Chris Lear Dec 06 '16 at 14:55
0

I think this will work for you.

var deepestLevel = 0;
var deepestLevelText = "";

function findDeepNested(element, currentLevel) {
    if ((element.children().length == 0) && (deepestLevel < currentLevel)) {
        // No children and current level is deeper than previous most nested level
        deepestLevelText="<li>" + element.text() + "</li>";
    }
    else { // there are children, keep diving
        element.children().each( function () {
            findDeepNested($(this), currentLevel + 1);
        });
    }
}

$(".start").each( function () {
    deepestLevel = 0;
    deepestLevelText = "";
    findDeepNested($(this), 0);
    $("#results").append(deepestLevelText);
});

Fiddle

Fabricio
  • 839
  • 9
  • 17
  • Won't this just print out all the leaf nodes, not just the deepest leaf node? Remember the OP's HTML example is not the general case with multiple branches at multiple depths. – jfriend00 Oct 08 '13 at 22:54
  • @jfriend00 This will only print out the text of nodes that have no children. As per the question Text 1 and Text 2 are required to be included in the result they are not at the same depth but they are the deepest nodes inside their structure. If the node is empty an empty LI will be added. But you can always edit the Fiddle I offered and test it with your desired case. Let me know your findings. – Fabricio Oct 08 '13 at 23:01
  • 1
    I don't think that's what the OP asked for. I think they want the single deepest node for each node passed in, not all leaf nodes. The OP's HTML shows an overly simple case, not the general case. Look at the HTML test case here: http://jsfiddle.net/jfriend00/8tC3a/ for example. – jfriend00 Oct 08 '13 at 23:07
  • Really, recursion with globals? Do you really think that meets the OP's request for the "best" way to do this. – jfriend00 Oct 09 '13 at 00:04
  • It's maybe not the cleanest programming technique but it does sort the problem in a way that can be reasonably read and understood. I wish JS could pass parameters by reference without having to use objects (yes I've read your question on the subject) but since it doesn't and using them would further complicate the code I went with global variables. – Fabricio Oct 09 '13 at 00:24
  • 2
    You could just pass them as arguments and then return them. Or put then on an object and pass a reference to the object. Either seems cleaner than using globals. Or, the cleanest would be to create a top level shell closure that defines them as local variables and then call a local function recursively rather than the top level function. The local function can refer to the closure variable like globals, but you aren't actually using globals. My answer uses local variables in a closure. – jfriend00 Oct 09 '13 at 00:38
  • 1
    Thanks for the tips. I'll do it tomorrow. It's 2:45AM here... :-) – Fabricio Oct 09 '13 at 00:47
-1
<script type="text/javascript">
    function findFarthestNode(node)
    {
        if(node.parent().length && node.parent().prop("tagName") != "BODY")
            return findFarthestNode(node.parent());
        return node;
    }
    jQuery(document).ready(function() {
        node = jQuery("span");
        farthest = findFarthestNode(node);
        console.log(farthest);
    });
</script>
Maciej Treder
  • 11,866
  • 5
  • 51
  • 74