How to find the nearest common ancestors of two or more nodes?

Solution 1:

Here's a pure JavaScript version that is a little more efficient.

function parents(node) {
  var nodes = [node]
  for (; node; node = node.parentNode) {
    nodes.unshift(node)
  }
  return nodes
}

function commonAncestor(node1, node2) {
  var parents1 = parents(node1)
  var parents2 = parents(node2)

  if (parents1[0] != parents2[0]) throw "No common ancestor!"

  for (var i = 0; i < parents1.length; i++) {
    if (parents1[i] != parents2[i]) return parents1[i - 1]
  }
}

Solution 2:

The solutions involving manually going through the ancestor elements are far more complicated than necessary. You don't need to do the loops manually. Get all the ancestor elements of one element with parents(), reduce it to the ones that contain the second element with has(), then get the first ancestor with first().

var a = $('#a'),
    b = $('#b'),
    closestCommonAncestor = a.parents().has(b).first();

jsFiddle example

Solution 3:

Here's another pure method that uses element.compareDocumentPosition() and element.contains(), the former being a standards method and the latter being a method supported by most major browsers excluding Firefox:

Comparing two nodes

function getCommonAncestor(node1, node2) {
    var method = "contains" in node1 ? "contains" : "compareDocumentPosition",
        test   = method === "contains" ? 1 : 0x10;

    while (node1 = node1.parentNode) {
        if ((node1[method](node2) & test) === test)
            return node1;
    }

    return null;
}

Working demo: http://jsfiddle.net/3FaRr/ (using lonesomeday's test case)

This should be, more or less, as efficient as possible since it is pure DOM and has only one loop.


Comparing two or more nodes

Taking another look at the question, I noticed the "or more" part of the "two or more" requirement had gone ignored by the answers. So I decided to tweak mine slightly to allow any number of nodes to be specified:

function getCommonAncestor(node1 /*, node2, node3, ... nodeN */) {
    if (arguments.length < 2)
        throw new Error("getCommonAncestor: not enough parameters");

    var i,
        method = "contains" in node1 ? "contains" : "compareDocumentPosition",
        test   = method === "contains" ? 1 : 0x0010,
        nodes  = [].slice.call(arguments, 1);

    rocking:
    while (node1 = node1.parentNode) {
        i = nodes.length;    
        while (i--) {
            if ((node1[method](nodes[i]) & test) !== test)
                continue rocking;
        }
        return node1;
    }

    return null;
}

Working demo: http://jsfiddle.net/AndyE/3FaRr/1

Solution 4:

Try this:

function get_common_ancestor(a, b)
{
    $parentsa = $(a).parents();
    $parentsb = $(b).parents();

    var found = null;

    $parentsa.each(function() {
        var thisa = this;

        $parentsb.each(function() {
            if (thisa == this)
            {
                found = this;
                return false;
            }
        });

        if (found) return false;
    });

    return found;
}

Use it like this:

var el = get_common_ancestor("#id_of_one_element", "#id_of_another_element");

That's just rattled out pretty quickly, but it should work. Should be easy to amend if you want something slightly different (e.g. jQuery object returned instead of DOM element, DOM elements as arguments rather than IDs, etc.)

Solution 5:

The commonAncestorContainer property of the he Range API mentioned above, alongside its selectNode, makes this a no-brainer.

Run ("display") this code in Firefox's Scratchpad or a similar editor:

var range = document.createRange();
var nodes = [document.head, document.body];  // or any other set of nodes
nodes.forEach(range.selectNode, range);
range.commonAncestorContainer;

Note that both APIs are not supported by IE 8 or below.