3

QUESTION:

I am trying to use JQuery's .find() to find all descendants within an element at any level that have a given attribute, but not the descendants of those descendants with the same attribute.

TO HELP UNDERSTANDING:

JQuery

The intended goal of the query below is find all descendants within element $("#some_id") (at any level) that have some_attribute attribute, but not the descendants of those descendants with the same attribute.

$("#some_id").find("[some_attribute]");

HTML

<span id="some_id" some_attribute>
    <span some_attribute> <!-- SELECT -->
        <span some_attribute> <!-- IGNORE -->
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
        <span>
        </span>
    </span>
    <span>
        <span some_attribute> <!-- SELECT -->
            <span>
                <span some_attribute> <!-- IGNORE -->
                </span>
            </span>
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
    </span>
    <span>
        <span>
            <span>
                <span some_attribute> <!-- SELECT -->
                </span>
            </span>
        </span>
    </span>
</span>

NOTE: I need a generic approach... I explain better! Suppose I don't know the selector $("#some_id") I only have the result of that query. Also consider that this result may refer to an element that may be within another element with the some_attribute attribute.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43

2 Answers2

3

You can use :not to make sure the selected elements do not match a particular selector - here, #some_id [some_attribute] [some_attribute] (because elements which match that selector will be a some_attribute nested in another some_attribute which is not the #some_id):

const result = $("#some_id")
  .find("[some_attribute]:not(#some_id [some_attribute] [some_attribute])")
  .each(function() {
    console.log(this.innerHTML);
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span id="some_id" some_attribute>
    <span some_attribute>sel <!-- SELECT -->
        <span some_attribute> <!-- IGNORE -->
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
        <span>
        </span>
    </span>
    <span>
        <span some_attribute>sel <!-- SELECT -->
            <span>
                <span some_attribute> <!-- IGNORE -->
                </span>
            </span>
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
    </span>
    <span>
        <span>
            <span>
                <span some_attribute>sel <!-- SELECT -->
                </span>
            </span>
        </span>
    </span>
</span>

If you can't hard-code it, I suppose a option would be to set an attribute so that you can use :not properly:

const elm = $("#some_id");
elm[0].dataset.parent = '';
elm
  .find("[some_attribute]:not([data-parent] [some_attribute] [some_attribute])")
  .each(function() {
    console.log(this.innerHTML);
  });
elm[0].removeAttribute('data-parent');
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span id="some_id" some_attribute>
    <span some_attribute>sel <!-- SELECT -->
        <span some_attribute> <!-- IGNORE -->
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
        <span>
        </span>
    </span>
    <span>
        <span some_attribute>sel <!-- SELECT -->
            <span>
                <span some_attribute> <!-- IGNORE -->
                </span>
            </span>
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
    </span>
    <span>
        <span>
            <span>
                <span some_attribute>sel <!-- SELECT -->
                </span>
            </span>
        </span>
    </span>
</span>

If you don't want to change the DOM either, .filter and check that the .closest element with [some_attribute] is the parent:

const elm = $("#some_id");
elm
  .find("[some_attribute]")
  .filter(function() {
    return $(this).parent().closest('[some_attribute]')[0] === elm[0];
  })
  .each(function() {
    console.log(this.innerHTML);
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span id="some_id" some_attribute>
    <span some_attribute>sel <!-- SELECT -->
        <span some_attribute> <!-- IGNORE -->
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
        <span>
        </span>
    </span>
    <span>
        <span some_attribute>sel <!-- SELECT -->
            <span>
                <span some_attribute> <!-- IGNORE -->
                </span>
            </span>
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
    </span>
    <span>
        <span>
            <span>
                <span some_attribute>sel <!-- SELECT -->
                </span>
            </span>
        </span>
    </span>
</span>

Using old versions of jQuery, you can use .selector:

const elm = $("#some_id");
const sel = elm.selector;
elm
  .find(`[some_attribute]:not(${sel} [some_attribute] [some_attribute])`)
  .each(function() {
    console.log(this.innerHTML);
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7/jquery.min.js"></script>
<span id="some_id" some_attribute>
    <span some_attribute>sel <!-- SELECT -->
        <span some_attribute> <!-- IGNORE -->
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
        <span>
        </span>
    </span>
    <span>
        <span some_attribute>sel <!-- SELECT -->
            <span>
                <span some_attribute> <!-- IGNORE -->
                </span>
            </span>
            <span some_attribute> <!-- IGNORE -->
            </span>
        </span>
    </span>
    <span>
        <span>
            <span>
                <span some_attribute>sel <!-- SELECT -->
                </span>
            </span>
        </span>
    </span>
</span>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I need a more generic approach. I explain better, suppose I don't know the selector `$("#some_id")` I only have the result of that query. Also consider that this result may refer to an element that may be within another element with the `some_attribute` attribute. Thanks! =D – Eduardo Lucio Oct 17 '19 at 22:29
  • Maybe some approach that prevents `.find()` from searching within the elements it has already encountered. =D – Eduardo Lucio Oct 17 '19 at 22:33
  • Here https://stackoverflow.com/q/38352374/3223785 there's a very similar question. The op presents an answer here https://jsfiddle.net/netaques/sc72fq1x/ . But, it doesn't seem like a good solution... =/ – Eduardo Lucio Oct 17 '19 at 22:37
  • 1
    See edits. You could also use `.selector` in older versions of jQuery, but it's not supported anymore (as of 1.9) – CertainPerformance Oct 17 '19 at 22:44
  • I'm testing and trying to understand why in my real case it is not working. I will give you feedback! =D – Eduardo Lucio Oct 21 '19 at 14:46
  • I tested the solution "If you don't want to change the DOM either [...]" ... But for some reason it didn't work for me in my "real code". I posted a solution on this thread that worked for me (`function findDescsNotDescsOfThose(jqPntElOrPntQry, jqFind)`). Thank you! =D – Eduardo Lucio Oct 21 '19 at 16:19
0

Here is my solution. For more explanation see the code notes below...

// NOTE: Find all descendants at any level, but not the descendants of those descendants.
// By Questor
function findDescsNotDescsOfThose(jqPntElOrPntQry, jqFind){
    var jqElInst;
    if (typeof jqPntElOrPntQry === "string" || 
            jqPntElOrPntQry instanceof String) {
    // [Ref.: https://stackoverflow.com/a/9436948/3223785 ]

        jqElInst = $(jqPntElOrPntQry);
    } else {
        jqElInst = jqPntElOrPntQry;
    }
    return jqElInst.find(jqFind)
        .filter(function(){

            // NOTE: We need use ".parent ()" and then ".closest ()", otherwise ".closest ()"
            // will find the element itself if it fits "jqFind". By Questor
            descOfjqFind = $(this).parent().closest(jqFind);

            // NOTE: Checks if it is not descended from any element that also fits
            // in "jqFind". Being descended from an element that also fits "jqFind"
            // checks to see if this element is descended from "jqElInst". By Questor
            if (descOfjqFind.length > 0 && $(descOfjqFind).
                    parent().closest(jqElInst).length === 1) {
                return false
            }

            return true;
    });
}

Thanks! =D

Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43