tags:

views:

153

answers:

4

How would I got about selecting the first parent of a set of elements that contains ALL of those elements?

For example:

<body>
 <dl>
  <dt>Items:</dt>
  <dd>
   <ul>
    <li>Item 1<div class="item-info">...</div></li>
    <li>Item 2<div class="item-info">...</div></li>
    <li>Item 3<div class="item-info">...</div></li>
   </ul>
  </dd>
 </dl>
</body>

I want something like this:

$('.item-info').commonParent();

and it would return the equivalent of:

[$('ul')]

Is there an easy way to do this with jQuery selectors? Or am I gonna have to write a plugin?

A: 

Are you looking for any of these?

$("element").parents() // return all parents until reaches 'html'

$("element").parent() // return the first parent

$("element").closest("parent") // return the closest selected parent
BrunoLM
It would be closest("ul") or parents("ul") since there's no "parent" element. That is, unless the OP is actually looking for the closest common element and that's another can of worms.
Andir
I think he's asking for the closest common ancestor, in which case this won't work.
You
Yeah, i was thinking $('.item-info').parent() or $('.item-info').closest('ul') when I read the question.
AndrewDotHay
+1  A: 

I assume the point is that the .item-info elements are (potentially) spread out throughout the page.

If that's right, try this: http://jsfiddle.net/EJWjf/1/

var $items = $('.item-info');   // Cache all the items in question
var total = $items.length;      // Cache the total number
var parent;                     // Will store the match

$items.first()           // Grab the first item (an arbitrary starting point)
    .parents()           // Get all of its parent elements
    .each(function() {
            // Iterate over each parent, finding the .info-item elements
            //    it contains, and see if the quantity matches the total
        if($(this).find('.item-info').length == total) {
            parent = this;  // If so, we found the closest common ancestor so
            return false;   //     store it and break out of the loop
        }
    });

alert(parent.tagName);

Here's a function version: http://jsfiddle.net/EJWjf/2/

function findCommon(selector) {
    var $items = $(selector);   
    var total = $items.length;      
    var parent;                   

    $items.first()
        .parents()
        .each(function() {
            if($(this).find(selector).length == total) {
                parent = this;
                return false;
            }
        });

    return $(parent);
}

var result = findCommon('.item-info');
patrick dw
This is a nice simple solution if he's only using one selector. But requires duplication/prior knowledge of the selector(s).
Jamie Wong
@Jamie - Please explain what you mean. You would have to know the selector at some point. No reason it couldn't be shared in a variable. Or am I not understanding what you mean?
patrick dw
@Jamie - Here's a function version: http://jsfiddle.net/EJWjf/2/ Just pass in the selector. Although you're right about multiple selectors. That would require a little more attention. Should be very doable, though.
patrick dw
Actually, works fine with multiple selectors, if you mean comma separated selectors. http://jsfiddle.net/EJWjf/4/
patrick dw
I meant more along the lines of if I have `var cont = $("some selector").parents("ul");` then later wanted to do `cont.commonAncestor()`. Chaining things is a really important part of jQuery. In any case, I was the one that upvoted you, I have no idea who downvoted you since they weren't helpful enough to leave a comment.
Jamie Wong
@Jamie - Thanks for the up vote. There's been more and more down votes without explanation lately. SO users seem to be getting a little low class unfortunately. :o( As far as chaining, this should be able to be made into a plugin just as easily. I wasn't sure what the OP ultimately wanted since a selector isn't an available option.
patrick dw
@Jamie - Added a plugin version, just for proof-of-concept. ;o) http://jsfiddle.net/EJWjf/5/
patrick dw
@patrick It's good to know about the .selector property, I didn't know about that! However, this still fails if you're not using simple selector. Example: `$('.item-info').add('span').findCommon();` fails
Jamie Wong
@Jamie - Very good point. Hadn't thought of that.
patrick dw
@patrick great, simple answer for the question provided. +1.
Alex
+5  A: 

If you are actually looking for lowest common ancesor:

jQuery.fn.commonAncestor = function() {
  var parents = [];
  var minlen = Infinity;

  $(this).each(function() {
    var curparents = $(this).parents();
    parents.push(curparents);
    minlen = Math.min(minlen, curparents.length);
  });

  for (var i in parents) {
    parents[i] = parents[i].slice(parents[i].length - minlen);
  }

  // Iterate until equality is found
  for (var i in parents[0]) {
    var equal = true;
    for (var j in parents) {
      if (parents[j][i] != parents[0][i]) {
        equal = false;
        break;
      }
    }
    if (equal) return $(parents[0][i]);
  }
  return $([]);
}

Example

<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"&gt;&lt;/script&gt;
<script src="jquery.commonancestor.js"></script>


$(function() {
  console.log($(".item-info").commonAncestor());
});
</script>
<body>
 <dl>
  <dt>Items:</dt>
  <dd>
   <ul>
    <li>Item 1<b><div class="item-info">...</div></b></li>
    <li>Item 2<u><div class="item-info">...</div></u></li>
    <li>Item 3<i><div class="item-info">...</div></i></li>
   </ul>
  </dd>
 </dl>
</body>

This has not been tested rigorously, so please point out any errors you see.

EDIT Was returning parent instead of $(parent)

Jamie Wong
This seems like it'd do the trick, however I'm wondering what's with all the slicing? Is it because `.parents()` returns the parent elements in reverse order, ending in `html`?
Bryan Ross
The slicing is required bring all the nodes to the same height in the DOM tree. If the element closest to the root (`html`) is 3 nodes away, then the lowest common ancestor cannot be more than 3 nodes away from the root. Also, doing equality checks on anything further away than that is pointless, since an element 3 nodes away from the root cannot possible be equal to an element 4 nodes away. The slice brings down the list of parent nodes to the minimum distance to the root node.
Jamie Wong
A: 

I believe this is what you're looking for:

jQuery

The first four lines contain the code that does the actual grabbing of the ULs. The rest is just to display the content of the matched ULs.

<script type="text/javascript">
  $(document).ready(function() {
    // The code that grabs the matching ULs
    var ULsWithAll=$("ul").has(".item-info").filter( function(index) { // Grab all ULs that has at least one .item-info
      var ChildrenOfUL=$(this).children("li");  // Grab the children

      return ChildrenOfUL.length===ChildrenOfUL.has(".item-info").length;  // Are all of them .item-info
    });

    $(ULsWithAll).each(function(){ // Output the ULs that were found
      alert($(this).html());
    })
  });
</script>

HTML

<dl>
  <dt>Items 1:</dt>
  <dd>
    <ul>
      <li>Item 1-1<div class="item-info">...</div></li>
      <li>Item 1-2<div class="item-info">...</div></li>
      <li>Item 1-3<div class="item-info">...</div></li>
    </ul>
  </dd>
  <dt>Items 2:</dt>
  <dd>
    <ul>
      <li>Item 2-1<div class="item-info">...</div></li>
      <li>Item 2-2<div class="item-info">...</div></li>
      <li>Item 2-3<div class="item-info">...</div></li>
    </ul>
  </dd>
  <dt>Items 3:</dt>
  <dd>
    <ul>
      <li>Item 3-1<div class="item-info">...</div></li>
      <li>Item 3-2<div class="item-info">...</div></li>
      <li>Item 3-3<div>...</div></li>
    </ul>
  </dd>
</dl>

The above jQuery code will return the ULs for the first and second hit (the third one is a dud). Check it at http://jsfiddle.net/ksTtF/

Another example (without the alert): http://jsfiddle.net/vQxGC/

Gert G
This is basically like patrick's answer, except longer, harder to understand and with more content duplication - i.e. making it useless if you wanted to reuse it.
Jamie Wong
@Jamie Wong - The code that actually does something is five lines. Sure, you can make one less jQuery element grab by putting `$(this).children("li")` in a variable.
Gert G
Okay, my issue with the length is invalid, but the other issues stand.
Jamie Wong
Changed it to filtering instead. Even more efficient.
Gert G