How to implement "prevUntil" in Vanilla JavaScript without libraries? - javascript

I need to implement the functionality of jQuery's prevUntil() method in Vanilla JavaScript.
I've got several <div> elements on the same level:
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
I'm trying to use an onclick event to find the event.target's previousSiblings until a certain criteria is reached (for example, a class name match) then stop.
How do I achieve this?

This answer was previously published here in response to a similar question .
There are a few ways to do it.
Either one of the following should do the trick.
// METHOD A (ARRAY.FILTER, STRING.INDEXOF)
var siblings = function(node, children) {
siblingList = children.filter(function(val) {
return [node].indexOf(val) != -1;
});
return siblingList;
}
// METHOD B (FOR LOOP, IF STATEMENT, ARRAY.PUSH)
var siblings = function(node, children) {
var siblingList = [];
for (var n = children.length - 1; n >= 0; n--) {
if (children[n] != node) {
siblingList.push(children[n]);
}
}
return siblingList;
}
// METHOD C (STRING.INDEXOF, ARRAY.SPLICE)
var siblings = function(node, children) {
siblingList = children;
index = siblingList.indexOf(node);
if(index != -1) {
siblingList.splice(index, 1);
}
return siblingList;
}
FYI: The jQuery code-base is a great resource for observing Grade A Javascript.
Here is an excellent tool that reveals the jQuery code-base in a very streamlined way.
http://james.padolsey.com/jquery/

Example
Using previousElementSibling:
var className = "needle";
var element = clickedElement;
while(element.previousElementSibling && element.previousElementSibling.className != className) {
element = element.previousElementSibling;
}
element.previousElementSibling; // the element or null

Use .children in combination with .parentNode. Then filter the NodeList, after converting it into an array: http://jsfiddle.net/pimvdb/DYSAm/.
var div = document.getElementsByTagName('div')[0];
var siblings = [].slice.call(div.parentNode.children) // convert to array
.filter(function(v) { return v !== div }); // remove element itself
console.log(siblings);

How about this:
while ( node = node.previousElementSibling ) {
if ( ( ' ' + node.className + ' ' ).indexOf( 'foo' ) !== -1 ) {
// found; do your thing
break;
}
}
Don't bother telling me that this doesn't work in IE8...

Just take a look at how jQuery does it.
prevUntil: function( elem, i, until ) {
return jQuery.dir( elem, "previousSibling", until );
},
Which uses a while / looping function caled dir(). The prevUntil just keeps going until previousSibling is the same as the until element.
dir: function( elem, dir, until ) {
var matched = [],
cur = elem[ dir ];
while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
if ( cur.nodeType === 1 ) {
matched.push( cur );
}
cur = cur[dir];
}
return matched;
},

There is a previousSibling property in the HTML DOM
Here is some reference
http://reference.sitepoint.com/javascript/Node/previousSibling

You could use indexOf to determine if the index of the siblings are less than the index of the target element:
// jshint esversion: 9
// get the target node
const node = document.querySelector("div:nth-child(3)");
// get the target node's index
const node_index = node.parentNode.indexOf(node);
// get the siblings of the target node
const siblings = node => [...node.parentNode.children].filter(child =>
child !== node
);
console.log(siblings);
// get the prevUntil
const class_name = "\bmy_class\b";
const prevUntil = siblings.filter((sibling, i) =>
i < node_index && (sibling.getAttribute("class") || "").includes(class_name)
);
console.log(prevUntil);
Good luck.

Related

Javascript function won't yield expected result

So I have a Javascript function that is supposed to yield the item's nth child. Below is the code for the function.
function findNthChild(element) {
nthchild = 1;
console.log(element);
if (element.prev().hasClass('.point')) {
while (element.prev().hasClass('.point')) {
nthchild++;
element == element.prev()
}
return nthchild;
}
else {
console.log('lmao');
return 1;
}
}
From your element, the algorithm you want might look something like this
Look at the next sibling element, elm
If elm is undefined or null return null
If elm has class interestingClass, decrease n by 1
If n is greater than 0 go back to step 1
Return elm
Vanilla,
function nthSiblingElementByClass(sibling, n, cls) {
if (sibling) do {
if (sibling.classList.contains(cls))
if (--n < 0) return sibling;
} while (sibling && (sibling = sibling.nextElementSibling));
return null;
}
Usage, e.g. on this page
var elm = document.querySelector('.kwd');
nthSiblingElementByClass(elm, 5, 'pln'); // <span class=​"pln">​console​</span>​

JQuery. Method to find closest common parent node of two jquery objects [duplicate]

Users selects two or more elements in a HTML page. What i want to accomplish is to find those elements' common ancestors (so body node would be the common ancestor if none found before) ?
P.S: It can be achieved with XPath but it is not a preferable option for me. Also it may be found with css selector parsing but i think it is a dirty method (?)
Thank you.
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]
}
}
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
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
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.
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.)
You should be able to use the jQuery .parents() function and then walk through the results looking for the first match. (Or I guess you could start from the end and go backwards until you see the first difference; that's probably better.)
You can also use a DOM Range (when supported by the browser, of course). If you create a Range with the startContainer set to the earlier node in the document and the endContainer set to the later node in the document, then the commonAncestorContainer attribute of such a Range is the deepest common ancestor node.
Here is some code implementing this idea:
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;
}
This doesn't require much code anymore to solve:
steps:
grab parent of node_a
if parent of node_a contains node_b return parent (in our code, the parent is referenced as node_a)
if parent does not contain node_b we need to keep going up the chain
end return null
code:
function getLowestCommonParent(node_a, node_b) {
while (node_a = node_a.parentElement) {
if (node_a.contains(node_b)) {
return node_a;
}
}
return null;
}
based on the answers from Andy E and AntonB
handle edge-cases: node1 == node2 and node1.contains(node2)
function getCommonParentNode(node1, node2) {
if (node1 == node2) return node1;
var parent = node1;
do if (parent.contains(node2)) return parent
while (parent = parent.parentNode);
return null;
}
This is a generalized take on lonesomeday's answer. Instead of only two elements it will take a full JQuery object.
function CommonAncestor(jq) {
var prnt = $(jq[0]);
jq.each(function () {
prnt = prnt.parents().add(prnt).has(this).last();
});
return prnt;
}
Somewhat late to the party, but here's an elegant jQuery solution (since the question is tagged 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();
};
Somewhat late to the party, here's a JavaScript ES6 version that uses Array.prototype.reduce() and Node.contains(), and can take any number of elements as parameters:
function closestCommonAncestor(...elements) {
const reducer = (prev, current) => current.parentElement.contains(prev) ? current.parentElement : prev;
return elements.reduce(reducer, elements[0]);
}
const element1 = document.getElementById('element1');
const element2 = document.getElementById('element2');
const commonAncestor = closestCommonAncestor(element1, element2);
Here is a dirtier way of doing this. It's easier to understand but requires dom modification:
function commonAncestor(node1,node2){
var tmp1 = node1,tmp2 = node2;
// find node1's 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's 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;
}
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;
}
did not liked any of the answers above(want pure javascript and one function).
that worked perfectly for me,efficient and also easier to understand:
const findCommonAncestor = (elem, elem2) => {
let parent1 = elem.parentElement,parent2 = elem2.parentElement;
let childrensOfParent1 = [],childrensOfParent2 = [];
while (parent1 !== null && parent2 !== null) {
if (parent1 !== !null) {
childrensOfParent2.push(parent2);
if (childrensOfParent2.includes(parent1)) return parent1;
}
if (parent2 !== !null) {
childrensOfParent1.push(parent1);
if (childrensOfParent1.includes(parent2)) return parent2;
}
parent1 = parent1.parentElement;
parent2 = parent1.parentElement;
}
return null;
};
Here is a better and shorter way of finding the common ancestor of two or more elements:
// find the common ancestor of two nodes
const findFirstCommonAncestor = (nodeA, nodeB) => {
if (nodeA.contains(nodeB)) return nodeA;
if (nodeB.contains(nodeA)) return nodeB;
const range = new Range();
range.setStartBefore(nodeA);
range.setEndAfter(nodeB);
if (range.collapsed) {
range.setStartBefore(nodeB);
range.setEndAfter(nodeA);
}
return range.commonAncestorContainer;
};
// find the common ancestor of multiple nodes
const firstFirstCommonAncestorMultiple = (nodes) =>
nodes.reduce((acc, node) => (acc === node ? acc : findFirstCommonAncestor(acc, node)), nodes[0]);

Why is this Javascript much *slower* than its jQuery equivalent?

I have a HTML list of about 500 items and a "filter" box above it. I started by using jQuery to filter the list when I typed a letter (timing code added later):
$('#filter').keyup( function() {
var jqStart = (new Date).getTime();
var search = $(this).val().toLowerCase();
var $list = $('ul.ablist > li');
$list.each( function() {
if ( $(this).text().toLowerCase().indexOf(search) === -1 )
$(this).hide();
else
$(this).show();
} );
console.log('Time: ' + ((new Date).getTime() - jqStart));
} );
However, there was a couple of seconds delay after typing each letter (particularly the first letter). So I thought it may be slightly quicker if I used plain Javascript (I read recently that jQuery's each function is particularly slow). Here's my JS equivalent:
document.getElementById('filter').addEventListener( 'keyup', function () {
var jsStart = (new Date).getTime();
var search = this.value.toLowerCase();
var list = document.querySelectorAll('ul.ablist > li');
for ( var i = 0; i < list.length; i++ )
{
if ( list[i].innerText.toLowerCase().indexOf(search) === -1 )
list[i].style.display = 'none';
else
list[i].style.display = 'block';
}
console.log('Time: ' + ((new Date).getTime() - jsStart));
}, false );
To my surprise however, the plain Javascript is up to 10 times slower than the jQuery equivalent. The jQuery version takes around 2-3 seconds to filter on each letter, while the Javascript version takes 17+ seconds! I'm using Google Chrome on Ubuntu Linux.
This isn't for anything really important so it doesn't need to be super efficient. But am I doing something really dumb with my Javascript here?
You could try using textContent instead of innerText , I think it should be faster. Also timing the list-generation and loop separately would tell if there is problem in list-generation.
Another best practice for javascript speed is caching the list.length in a variable and calling the variable like:
l = list.length;
for (var i=0;i<l;i++):{ code here}
And maybe timing with jsperf would be better.
Here, I've refactored your code a bit:
var filter = document.getElementById( 'filter' ),
ablist = document.querySelector( '.ablist' );
filter.addEventListener( 'keyup', function () {
var re, elems, i, len, elem;
re = RegExp( this.value, 'i' );
elems = ablist.children;
for ( i = 0, len = elems.length; i < len; i += 1 ) {
elem = elems[i];
elem.style.display =
elem.textContent.search( re ) > -1 ? 'list-item' : 'none';
}
}, false );
Live demo: http://jsfiddle.net/MVFxn/
Changes:
with a regular expression and an i flag, there's no need for toLowerCase,
if there is only one '.ablist' element on the page, querySelector should be the fastest way to grab it (since it aborts the query once it finds the first such element),
there's no query for the LI elements since the children property already references them conveniently.
I'd love to know how this code performs on your page...
I used while instead of for and did some minor improvements. Here is the final code.
var list = list = document.querySelectorAll('ul.ablist > li');
document.getElementById('javascriptFilter').addEventListener( 'keyup', function () {
var jsStart = (new Date).getTime(),
search = this.value.toLowerCase(),
i = list.length - 1,
listItem,
result;
while( i >= 0 )
{
listItem = list[i];
if ( listItem.textContent.toLowerCase().indexOf(search) === -1 )
listItem.style.display = 'none';
else
listItem.style.display = 'block';
i--;
}
result = ((new Date).getTime() - jsStart);
console.log(['Time: ', result, '<br />'].join(''));
}, false );

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

Users selects two or more elements in a HTML page. What i want to accomplish is to find those elements' common ancestors (so body node would be the common ancestor if none found before) ?
P.S: It can be achieved with XPath but it is not a preferable option for me. Also it may be found with css selector parsing but i think it is a dirty method (?)
Thank you.
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]
}
}
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
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
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.
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.)
You should be able to use the jQuery .parents() function and then walk through the results looking for the first match. (Or I guess you could start from the end and go backwards until you see the first difference; that's probably better.)
You can also use a DOM Range (when supported by the browser, of course). If you create a Range with the startContainer set to the earlier node in the document and the endContainer set to the later node in the document, then the commonAncestorContainer attribute of such a Range is the deepest common ancestor node.
Here is some code implementing this idea:
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;
}
This doesn't require much code anymore to solve:
steps:
grab parent of node_a
if parent of node_a contains node_b return parent (in our code, the parent is referenced as node_a)
if parent does not contain node_b we need to keep going up the chain
end return null
code:
function getLowestCommonParent(node_a, node_b) {
while (node_a = node_a.parentElement) {
if (node_a.contains(node_b)) {
return node_a;
}
}
return null;
}
based on the answers from Andy E and AntonB
handle edge-cases: node1 == node2 and node1.contains(node2)
function getCommonParentNode(node1, node2) {
if (node1 == node2) return node1;
var parent = node1;
do if (parent.contains(node2)) return parent
while (parent = parent.parentNode);
return null;
}
This is a generalized take on lonesomeday's answer. Instead of only two elements it will take a full JQuery object.
function CommonAncestor(jq) {
var prnt = $(jq[0]);
jq.each(function () {
prnt = prnt.parents().add(prnt).has(this).last();
});
return prnt;
}
Somewhat late to the party, but here's an elegant jQuery solution (since the question is tagged 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();
};
Somewhat late to the party, here's a JavaScript ES6 version that uses Array.prototype.reduce() and Node.contains(), and can take any number of elements as parameters:
function closestCommonAncestor(...elements) {
const reducer = (prev, current) => current.parentElement.contains(prev) ? current.parentElement : prev;
return elements.reduce(reducer, elements[0]);
}
const element1 = document.getElementById('element1');
const element2 = document.getElementById('element2');
const commonAncestor = closestCommonAncestor(element1, element2);
Here is a dirtier way of doing this. It's easier to understand but requires dom modification:
function commonAncestor(node1,node2){
var tmp1 = node1,tmp2 = node2;
// find node1's 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's 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;
}
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;
}
did not liked any of the answers above(want pure javascript and one function).
that worked perfectly for me,efficient and also easier to understand:
const findCommonAncestor = (elem, elem2) => {
let parent1 = elem.parentElement,parent2 = elem2.parentElement;
let childrensOfParent1 = [],childrensOfParent2 = [];
while (parent1 !== null && parent2 !== null) {
if (parent1 !== !null) {
childrensOfParent2.push(parent2);
if (childrensOfParent2.includes(parent1)) return parent1;
}
if (parent2 !== !null) {
childrensOfParent1.push(parent1);
if (childrensOfParent1.includes(parent2)) return parent2;
}
parent1 = parent1.parentElement;
parent2 = parent1.parentElement;
}
return null;
};
Here is a better and shorter way of finding the common ancestor of two or more elements:
// find the common ancestor of two nodes
const findFirstCommonAncestor = (nodeA, nodeB) => {
if (nodeA.contains(nodeB)) return nodeA;
if (nodeB.contains(nodeA)) return nodeB;
const range = new Range();
range.setStartBefore(nodeA);
range.setEndAfter(nodeB);
if (range.collapsed) {
range.setStartBefore(nodeB);
range.setEndAfter(nodeA);
}
return range.commonAncestorContainer;
};
// find the common ancestor of multiple nodes
const firstFirstCommonAncestorMultiple = (nodes) =>
nodes.reduce((acc, node) => (acc === node ? acc : findFirstCommonAncestor(acc, node)), nodes[0]);

What's the best way to find the first common parent of two DOM nodes in javascript?

My question is exactly that but in context I want to examine the selection object, compare the anchorNode and focusNode and if they are different then find the first common parent element.
var selected = window.getSelection();
var anchor = selection.anchorNode;
var focus = selection.focusNode;
if ( anchor != focus ) {
// find common parent...
}
I would try something like this, assuming no JS library:
function findFirstCommonAncestor(nodeA, nodeB, ancestorsB) {
var ancestorsB = ancestorsB || getAncestors(nodeB);
if(ancestorsB.length == 0) return null;
else if(ancestorsB.indexOf(nodeA) > -1) return nodeA;
else if(nodeA == document) return null;
else return findFirstCommonAncestor(nodeA.parentNode, nodeB, ancestorsB);
}
using this utilities:
function getAncestors(node) {
if(node != document) return [node].concat(getAncestors(node.parentNode));
else return [node];
}
if(Array.prototype.indexOf === undefined) {
Array.prototype.indexOf = function(element) {
for(var i=0, l=this.length; i<l; i++) {
if(this[i] == element) return i;
}
return -1;
};
}
Then you can call findFirstCommonAncestor(myElementA, myElementB).
Since this question and accepted answer are very dated, I'd like to suggest using a more modern DOM API, Range:
function findFirstCommonAncestor(nodeA, nodeB) {
let range = new Range();
range.setStart(nodeA, 0);
range.setEnd(nodeB, 0);
// There's a compilication, if nodeA is positioned after
// nodeB in the document, we created a collapsed range.
// That means the start and end of the range are at the
// same position. In that case `range.commonAncestorContainer`
// would likely just be `nodeB.parentNode`.
if(range.collapsed) {
// The old switcheroo does the trick.
range.setStart(nodeB, 0);
range.setEnd(nodeA, 0);
}
return range.commonAncestorContainer;
}
This way is fairly straightforward:
var fp = $(focus).parents();
var ap = $(anchor).parents();
for (var i=0; i<ap.length; i++) {
if (fp.index(ap[i]) != -1) {
// common parent
}
}
Loop through the parents() of one element and see if they are contained in the parents() of the other using index() until you find a match (or not).
// It seems like it should be fairly simple, even without a library or indexOf
document.commonParent= function(a, b){
var pa= [], L;
while(a){
pa[pa.length]=a;
a= a.parentNode;
}
L=pa.length;
while(b){
for(var i=0; i<L; i++){
if(pa[i]==b) return b;
}
b= b.parentNode;
}
}
There is a a good bit of DOM API for that: compareDocumentPosition
This is how it goes:
/**
* Returns closest parent element for both nodes.
*/
function getCommonParent(one, two){
let parent = one.parentElement;
if(one === two) { //both nodes are the same node.
return parent;
}
const contained = Node.DOCUMENT_POSITION_CONTAINED_BY;
let docpos = parent.compareDocumentPosition(two);
while(parent && !(docpos & contained)) {
parent = parent.parentElement;
docpos = parent.compareDocumentPosition(two);
}
return parent;
}

Categories