3

I am looking to select the common parent of multiple nested elements for which I only know the inner text.

For example, in the following code:

<unknown>       
    <unknown class="unknown">
        ....
        <unknown>
            <unknown>Sometext</unknown>
        </unknown>
        <unknown>
            <unknown>Sometext</unknown>
        </unknown>
        <unknown>
            <unknown>Sometext</unknown>
        </unknown>
        ....
    </unknown>
</unknown>

I would like to get the closest element (common parent) that has class unknown in this scenario. I do not know the actual tags or class names. I only know that the nest element contains the "Sometext". I know this can be done through a loop using jQuery/Javascript, but is there a CSS selector that I can use with jQuery to find this? I tried using a combination of closest(), parents(), parentsUntil() but I can't seem to get to this element.

Thank you!

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356

2 Answers2

3

First, you need to ensure you only match the leaf nodes (nodes with no child nodes), so use:

:not(:has(*))

So to find all the exact matches (just the leaf nodes), use:

var matches = $(':not(:has(*))').filter(function () {
    return $(this).text() == "Sometext";
});

or just using a combined filter on all elements (with an added check for 0 children):

var matches = $('*').filter(function () {
     return !$(this).children().length && $(this).text() == "Sometext";
});

Note: I have not yet tested which of these two options is fastest.

Then you need to find the first ancestor (of the first match), that contains all the matches:

var commonparent = matches.first().parents().filter(function () {
    return $(this).find(matches).length == matches.length;
}).first();

JSFiddle: http://jsfiddle.net/TrueBlueAussie/v4gr1ykg/

Based on David Thomas' suggestion, here it is as a pair of jQuery extensions (commonParents() and commonParent()) that may be of future use for people:

To find all common parents of a jQuery collection use `commonParents()':

$.fn.commonParents = function (){
    var cachedThis = this;
    return cachedThis.first().parents().filter(function () {
        return $(this).find(cachedThis).length === cachedThis.length;
    });
};

JSFiddle: (commonParents): http://jsfiddle.net/TrueBlueAussie/v4gr1ykg/3/

To find the closest common parent of a jQuery collection use commonParent():

$.fn.commonParent = function (){
    return $(this).commonParents().first();
};

JSFiddle: (commonParent): http://jsfiddle.net/TrueBlueAussie/v4gr1ykg/2/

Notes:

  • jQuery optimises the combined use of first() in commonParent with the commonParents filter() and it only calls the code in commonParents until a first match is made, so commonParent does not need to be made more efficient.
iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • Nicely done, here's a version of your approach rewritten in plugin form: [JS Fiddle demo](http://jsfiddle.net/davidThomas/v4gr1ykg/1/). – David Thomas Jan 01 '15 at 21:54
  • @David Thomas: Nicely wrapped as an extension. I had not considered this to be required often enough to make into an extension, but I will add my own extensions to the answer. – iCollect.it Ltd Jan 01 '15 at 22:05
  • Yeah, it's one of those that'll perhaps come up relatively infrequently, I'd imagine, but I saw your answer and had a couple of moments...now, if you can work likely search-phrases into your answer the long-tail on this should be impressive. :) – David Thomas Jan 01 '15 at 22:36
  • Awesome answer TrueBlueAusie. Thank you for explaining the process in detail. – Nikolaos Alexiou Jan 02 '15 at 02:33
0

This should do the job. You basically find all the relevant parents of all the matching elements, get the intersection of each of those sets, then grab the first one to get the most nested common parent.

You can even wrap it up as as jquery plugin.

if(console && console.clear) console.clear();

// create a handy intersection method for Arrays
// see http://stackoverflow.com/a/16227294/1901857
Array.prototype.intersect = function(arr) {
    var a = this, b = arr;
    var t;
    if (b.length > a.length) t = b, b = a, a = t; // indexOf to loop over shorter
    return a.filter(function (e) {
        return b.indexOf(e) > -1;
    });
};

;(function($) {
    $.fn.commonParents = function(selector) {
        // find all relevant parents for each element and get set intersection
        // pushStack means you can use end() etc in chaining correctly
        return this.pushStack(sometexts.get().reduce(function(prevParents, el) {
            // common parents for this element - note the lowest level parent is first
            var parents = $(el).parents(selector || '*').get();

            // intersect with the previous value (or itself if first)
            return (prevParents || parents).intersect(parents);
        }, null), "commonParents", arguments);
    };
})(jQuery);


// text to search for
var search = "Sometext";
// parent selector to filter parents by e.g. '.unknown' - use null for all parents
var parentSelector = null;

// find everything containing search
var sometexts = $(":contains('" + search + "')").filter(function() { return $(this).text() == search; });

// grab the first common parent - the lowest level one - or null if there isn't one
var commonParent = sometexts.commonParents(parentSelector).get(0);

console.log(commonParent);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div>
    <div class="unknown test">
        <div class="unknown test2">
            <div class="unknown">
                <div>Sometext</div>
            </div>
            <div>
                <div>Sometext</div>
            </div>
            <div>
                <div>Sometext</div>
            </div>
        </div>
    </div>
</div>
Rhumborl
  • 16,349
  • 4
  • 39
  • 45
  • Thanks Rhumborl. This would work if I actually knew the Parent's class. However, I named unknown because it varies. This is what I came up with to get what I need but I am thinking it can be improved/refactored: `var jq = $('*').find(':last:contains("Sometext")'); var prnt = $(jq[0]); jq.each(function () { prnt = prnt.parents().add(prnt).has(this).last(); }); return prnt;` – Nikolaos Alexiou Dec 31 '14 at 23:25
  • Oh right so the class bit is actually irrelevant? If you just want _the_ common parent, just set parentClass to `*`. – Rhumborl Dec 31 '14 at 23:31