5

Solved (sort of)

Well, I think I solved it (barring edge cases) using the following:

function findByDepth(parent, child, depth){
    var children = $();
    $(child, $(parent)).each(function(){
        if($(this).parentsUntil(parent, child).length == (depth - 1)){
            children = $(children).add($(this));
        }
    });
    return children;
}

// this would successfully return the 3.X elements of an HTML snippet structured
// as my XML example, where <parent> = #parent, etc.
var threeDeep = findByDepth('#parent', '.child', 3);

However somebody has to get the accepted answer here, and I'm not going to answer it myself and abscond with your well earned rep. So, if anyone wants to add anything, such as provide insight into optimizing this function (before I go and $.extend() it in) I'll likely mark your answer correct, otherwise falling back to marking whoever was first on my initial question.

By the way, check it in the fiddle: http://jsfiddle.net/5PDaA/

Sub-update

Looking again at @CAFxX's answer, I realized that his approach is probably faster, taking advantage of querySelectorAll in browsers that can. Anyways, I revised his approach to the following, yet it's still giving me guff:

$.fn.extend({
    'findAtDepth': function(selector, depth){
        var depth = parseInt(depth) || 1;
        var query = selector;
        for(var i = 1; i < depth; i++){
            query += (' ' + selector);
        }
        return $(query, $(this)).not(query + ' ' + selector);
    }
});

It works fine the first time, however as context changes to an element found in the selector, it fails for some reason.


Update

Alright, I was foolishly unclear, and uninformed as to the specs for what I'm doing. Since I've reviewed my implementation I'll update here; I'll mark the earliest answer that meets my initial requirements given people think me a fool for updating so gratuitously (I wouldn't blame you) however my bonus mention below is essentially a requirement. I'd post another question, but it'd likely get closed from duplication. Regardless, +1's all around for your patience:

Depth specification of a given child is necessary (given it's wrapped in a function or otherwise) thus isolating a child and equally nested (not necessarily siblings) matching elements.

For instance (XML for brevity):

<!-- depth . sibling-index-with-respect-to-depth -->

<parent>
    <child>                     <!-- 1.1 -->
        <child>                 <!-- 2.1 -->
            <child>             <!-- 3.1 -->
                <child></child> <!-- 4.1 -->
            </child>            
            <child>             <!-- 3.2 -->
                <child></child> <!-- 4.2 -->
            </child>            
        </child>                
        <child>                 <!-- 2.2 -->
            <child></child>     <!-- 3.3 -->
        </child>                
    </child>                    
</parent>

Given a depth specified of 2, all 2.X elements are selected. Given 4 all 4.X, and so on.


Original Question

Using the native functionality of jQuery, is there a way to select only the "first-generation" of descendants matching a selector? For instance:

Note: The following is only an example. .child elements are nested in a parent at an arbitrary level.

Bonus: As my proposed syntax below indicates, an answer that provides a way to specify a depth to which the selection should traverse would be incredible.

// HTML
<div id="parent">
    <div>
        <div class="child"> <!-- match -->
            <div>
                <div class="child"> <!-- NO match -->
                </div>
            </div>
        </div>
        <div class="child"> <!-- match -->
            <div>
                <div class="child"> <!-- NO match -->
                </div>
            </div>
        </div>
    </div>
</div>

And:

// jQuery
$('#parent').find('.child:generation(1)'); // something in that vein

Trying to select from the context of #parent, the jQuery :first doesn't work here as it only hits the first matched .child.

Dan Lugg
  • 20,192
  • 19
  • 110
  • 174
  • 2
    is the depth of child arbitrary, or will it always be in a div? `#parent>div>.child` will work for this example – Andy Ray Sep 17 '11 at 06:06
  • Thanks @Andy Ray - Just updated to reflect; yes, arbitrary depth. Could be an immediate child, could be several levels deep. – Dan Lugg Sep 17 '11 at 06:09

3 Answers3

2

Try this (KISS!):

$("#parent .child").not(".child .child");

edit: to get the second level:

$("#parent .child .child").not(".child .child .child");

the third:

$("#parent .child .child .child").not(".child .child .child .child");

and so on... so you could have (untested):

function findChildrenDeepDown(parentSelector, level) {
  level = parseInt(level);
  var firstSelctor = parent, notSelector = ".child", i;
  for (i=0; i<level; i++) {
    firstSelector += " .child";
    notSelector += " .child";
  }
  return $(firstSelector).not(notSelector);
}

findChildrenDeepDown("#parent", 3); // find third-level children of #parent
CAFxX
  • 28,060
  • 6
  • 41
  • 66
  • Thanks @CAFxX - That appears to do the job. Any suggestions on the *bonus* I updated above? Perhaps some sort of iteration using the `.not()` or `:not()` method? – Dan Lugg Sep 17 '11 at 06:15
  • 1
    there you go, not very elegant but it should work. you could wrap it in a function that concatenates `" .child"` N times (with N the desired depth) and then invokes the `$()`. – CAFxX Sep 17 '11 at 06:18
  • You know, this is probably the least computationally expensive method. Traversing the tree both ways, rifling through elements to isolate a given depth can get expensive, but this shouldn't be. – Dan Lugg Sep 17 '11 at 06:21
  • There is a more elegant way : `$("#parent").find(".child").siblings();` (as said in my post) – Arnaud F. Sep 17 '11 at 06:35
  • Hm, doesn't appear to be working; the `.not()` clause is interfering with a depth specification greater than 1. – Dan Lugg Sep 17 '11 at 06:35
  • Thanks @CAFxX - Still appears to be a problem (*I know you mentioned your edit is untested, however I revised it with no luck still*) I'll post it up in the question, as I'm still having trouble with this; my proposed solution doesn't appear to be working properly either. – Dan Lugg Sep 17 '11 at 15:59
  • @Bracketworks I guess your `findAtDepth` doesn't work because the `.not()` is not relative to the element you're starting from. Suppose you execute it on a .parent .child .child, asking for `.child` 2 layers deeper: the `$()` would correctly return them (along their `.child` descendants), but the `.not(".child .child .child")` would filter them all out because they really are `.parent .child .child .child .child .child` – CAFxX Sep 18 '11 at 06:12
1

you can get it like this :

$("#parent").find(">div>.child")

EDIT

If first child can be in a random depth, you can use

$("#parent").find(".child").siblings();
Arnaud F.
  • 8,252
  • 11
  • 53
  • 102
  • Thanks @Arnaud F. - As per Andy's comment, I updated the question; the `.child` elements can exist an an arbitrary depth with respect to the `#parent`. Also, to the -1'er; likely not warranted, his answer was pre-update. – Dan Lugg Sep 17 '11 at 06:10
  • this will only work if you are guaranteed to have two top level children to find, and if there aren't any bottom level children that are next to each other – Andy Ray Sep 17 '11 at 06:39
1

In general, I think you'd have to find all of the .child elements and then throw away those that have .child ancestors:

$('#parent .child').filter(function() {
    return $(this).parent().closest('.child').length == 0;
});

This doesn't handle things like this though:

<div id="parent">
    <div class="child"> <!-- match -->
        <div>
            <div class="child"> <!-- NO match -->
            </div>
        </div>
    </div>
    <div>
        <div>
            <div class="child"> <!-- NO match wanted but it will match -->
            </div>
        </div>
    </div>
</div>

but maybe it will be sufficient for the structure of your HTML.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • Thanks @mu - This is moving in a good direction; CAFxX's response solves the initial problem, and certainly is KISS-able, however a programmatic approach will provide more flexibility as my *bonus* update calls for. – Dan Lugg Sep 17 '11 at 06:19
  • Big ole' pile of nested elements, but as my *bonus* mentions (*which is really more of a requirement as I consider the implementation*) the ability to specify the depth with respect to the selector (*`.child` or whatever*) would be great. I see where you're going; probably `$('.child:first').siblings('.child')`, but depth specification would be great. – Dan Lugg Sep 17 '11 at 06:28
  • @Bracketworks: You could skip jQuery and do a BFS through the DOM to find your starting `.child` and then go sideways to get the rest at that depth; then pile 'em all together with `x=$();x.add()`; might not be pretty but it should work. BTW, answering your own question is fine, as is accepting your own answer (there's even a [badge](http://stackoverflow.com/badges/14/self-learner) for that sort of thing). – mu is too short Sep 17 '11 at 22:09