Подтвердить что ты не робот

Как найти ближайших общих предков двух или более узлов?

Пользователи выбирают два или более элемента на странице HTML. То, что я хочу выполнить, - найти общих предков этих элементов (так что тело node будет общим предком, если его не найти раньше)?

P.S: Этого можно достичь с помощью XPath, но это не предпочтительный вариант для меня. Также он может быть найден с помощью селектора css, но я думаю, что это грязный метод (?)

Спасибо.

4b9b3361

Ответ 1

Попробуйте следующее:

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;
}

Используйте его следующим образом:

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

Это очень быстро, но это должно сработать. Если вам нужно что-то немного другое (например, объект jQuery, возвращенный вместо элемента DOM, элементы DOM в качестве аргументов, а не идентификаторы и т.д.)

Ответ 2

Здесь чистая версия JavaScript, которая немного более эффективна.

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]
  }
}

Ответ 3

Решения, связанные с ручным прохождением элементов предка, намного сложнее, чем необходимо. Вам не нужно делать петли вручную. Получите все элементы-предки одного элемента с parents(), уменьшите его до тех, которые содержат второй элемент с has(), затем получите первого предка с first().

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

Пример jsFiddle

Ответ 4

Вот еще один чистый метод, который использует element.compareDocumentPosition() и element.contains(), первый из которых - метод стандартов, а второй - метод, поддерживаемый большинством основных браузеров, за исключением Firefox:

Сравнивая два узла

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;
}

Рабочая демонстрация: http://jsfiddle.net/3FaRr/ (с использованием тестового примера одинокого дня)

Это должно быть более или менее максимально эффективным, так как это чистый DOM и имеет только один цикл.


Сравнение двух или более узлов

Еще раз посмотрев на вопрос, я заметил, что "или более" часть требования "два или более" проигнорирована ответами. Поэтому я решил немного подправить мой, чтобы можно было указать любое количество узлов:

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;
}

Рабочая демонстрация: http://jsfiddle.net/AndyE/3FaRr/1

Ответ 5

commonAncestorContainer свойство вышеупомянутого API диапазона, наряду с selectNode, делает это без проблем.

Запустить ( "показать" ) этот код в Firefox Scratchpad или аналогичный редактор:

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

Обратите внимание, что оба API не поддерживаются IE 8 или ниже.

Ответ 6

Вы должны иметь возможность использовать функцию jQuery .parents(), а затем просмотреть результаты поиска первого совпадения. (Или, я думаю, вы могли бы начать с конца и идти назад, пока не увидите первое различие, вероятно, лучше.)

Ответ 7

Вы также можете использовать DOM Range (при поддержке браузера, конечно). Если вы создаете Range с startContainer, установленным в предыдущем node в документе, а endContainer - более поздним node в документе, тогда commonAncestorContainer атрибут такого Range является самым глубоким общим предком node.

Вот какой код реализует эту идею:

function getCommonAncestor(node1, node2) {
    var dp = node1.compareDocumentPosition(node2);
    // If the bitmask includes the DOCUMENT_POSITION_DISCONNECTED bit, 0x1, or the
    // DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC bit, 0x20, then the order is implementation
    // specific.
    if (dp & (0x1 | 0x20)) {
        if (node1 === node2) return node1;

        var node1AndAncestors = [node1];
        while ((node1 = node1.parentNode) != null) {
            node1AndAncestors.push(node1);
        }
        var node2AndAncestors = [node2];
        while ((node2 = node2.parentNode) != null) {
            node2AndAncestors.push(node2);
        }

        var len1 = node1AndAncestors.length;
        var len2 = node2AndAncestors.length;

        // If the last element of the two arrays is not the same, then `node1' and `node2' do
        // not share a common ancestor.
        if (node1AndAncestors[len1 - 1] !== node2AndAncestors[len2 - 1]) {
            return null;
        }

        var i = 1;
        for (;;) {
            if (node1AndAncestors[len1 - 1 - i] !== node2AndAncestors[len2 - 1 - i]) {
                // assert node1AndAncestors[len1 - 1 - i - 1] === node2AndAncestors[len2 - 1 - i - 1];
                return node1AndAncestors[len1 - 1 - i - 1];
            }
            ++i;
        }
        // assert false;
        throw "Shouldn't reach here!";
    }

    // "If the two nodes being compared are the same node, then no flags are set on the return."
    // http://www.w3.org/TR/DOM-Level-3-Core/core.html#DocumentPosition
    if (dp == 0) {
        // assert node1 === node2;
        return node1;

    } else if (dp & 0x8) {
        // assert node2.contains(node1);
        return node2;

    } else if (dp & 0x10) {
        // assert node1.contains(node2);
        return node1;
    }

    // In this case, `node2' precedes `node1'. Swap `node1' and `node2' so that `node1' precedes
    // `node2'.
    if (dp & 0x2) {
        var tmp = node1;
        node1 = node2;
        node2 = tmp;
    } else {
        // assert dp & 0x4;
    }

    var range = node1.ownerDocument.createRange();
    range.setStart(node1, 0);
    range.setEnd(node2, 0);
    return range.commonAncestorContainer;
}

Ответ 8

Это обобщенный ответ на вопрос о lonesomeday. Вместо двух элементов он будет иметь полный объект JQuery.

function CommonAncestor(jq) {
    var prnt = $(jq[0]);
    jq.each(function () { 
        prnt = prnt.parents().add(prnt).has(this).last(); 
    });
    return prnt;
}

Ответ 9

Несколько поздно для вечеринки, но здесь элегантное решение jQuery (так как вопрос отмечен jQuery) -

/**
 * Get all parents of an element including itself
 * @returns {jQuerySelector}
 */
$.fn.family = function() {
    var i, el, nodes = $();

    for (i = 0; i < this.length; i++) {
        for (el = this[i]; el !== document; el = el.parentNode) {
            nodes.push(el);
        }
    }
    return nodes;
};

/**
 * Find the common ancestors in or against a selector
 * @param selector {?(String|jQuerySelector|Element)}
 * @returns {jQuerySelector}
 */
$.fn.common = function(selector) {
    if (selector && this.is(selector)) {
        return this;
    }
    var i,
            $parents = (selector ? this : this.eq(0)).family(),
            $targets = selector ? $(selector) : this.slice(1);
    for (i = 0; i < $targets.length; i++) {
        $parents = $parents.has($targets.eq(i).family());
    }
    return $parents;
};

/**
 * Find the first common ancestor in or against a selector
 * @param selector {?(String|jQuerySelector|Element)}
 * @returns {jQuerySelector}
 */
$.fn.commonFirst = function(selector) {
    return this.common(selector).first();
};

Ответ 10

Вот более грязный способ сделать это. Это проще понять, но требует модификации dom:

function commonAncestor(node1,node2){
        var tmp1 = node1,tmp2 = node2;
        // find node1 first parent whose nodeType == 1
        while(tmp1.nodeType != 1){
            tmp1 = tmp1.parentNode;
        }
        // insert an invisible span contains a strange character that no one 
        // would use
        // if you need to use this function many times,create the span outside
        // so you can use it without creating every time
        var span = document.createElement('span')
            , strange_char = '\uee99';
        span.style.display='none';
        span.innerHTML = strange_char;
        tmp1.appendChild(span);
        // find node2 first parent which contains that odd character, that 
        // would be the node we are looking for
        while(tmp2.innerHTML.indexOf(strange_char) == -1){
            tmp2 = tmp2.parentNode; 
        }                           
        // remove that dirty span
        tmp1.removeChild(span);
        return tmp2;
    }

Ответ 11

Для этого больше не требуется много кода:

действия:

  • захватить родительский узел node_a
  • Если родительский элемент node_a содержит node_b return parent (в нашем коде родитель ссылается как node_a)
  • Если родительский элемент не содержит node_b, нам нужно продолжать движение по цепочке
  • end return null

код:

function getLowestCommonParent(node_a, node_b) {
    while (node_a = node_a.parentElement) {
        if (node_a.contains(node_b)) {
            return node_a;
        }
    }

    return null;
}

Ответ 12

PureJS

function getFirstCommonAncestor(nodeA, nodeB) {
    const parentsOfA = this.getParents(nodeA);
    const parentsOfB = this.getParents(nodeB);
    return parentsOfA.find((item) => parentsOfB.indexOf(item) !== -1);
}

function getParents(node) {
    const result = [];
    while (node = node.parentElement) {
        result.push(node);
    }
    return result;
}