Как найти ближайших общих предков двух или более узлов?
Пользователи выбирают два или более элемента на странице HTML. То, что я хочу выполнить, - найти общих предков этих элементов (так что тело node будет общим предком, если его не найти раньше)?
P.S: Этого можно достичь с помощью XPath, но это не предпочтительный вариант для меня. Также он может быть найден с помощью селектора css, но я думаю, что это грязный метод (?)
Спасибо.
Ответы
Ответ 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;
}