In straight up javascript (i.e., no extensions such as jQuery, etc.), is there a way to determine a child node's index inside of its parent node without iterating over and comparing all children nodes?
E.g.,
var child = document.getElementById('my_element');
var parent = child.parentNode;
var childNodes = parent.childNodes;
var count = childNodes.length;
var child_index;
for (var i = 0; i < count; ++i) {
if (child === childNodes[i]) {
child_index = i;
break;
}
}
Is there a better way to determine the child's index?
This question is related to
javascript
dom
<body>
<section>
<section onclick="childIndex(this)">child a</section>
<section onclick="childIndex(this)">child b</section>
<section onclick="childIndex(this)">child c</section>
</section>
<script>
function childIndex(e){
let i = 0;
while (e.parentNode.children[i] != e) i++;
alert('child index '+i);
}
</script>
</body>
ES—Shorter
[...element.parentNode.children].indexOf(element);
The spread Operator is a shortcut for that
Array.from(element.parentNode.children).indexOf(element)
element.parentNode.children
? Returns the brothers of element
, including that element.
Array.from
? Casts the constructor of children
to an Array
object
indexOf
? You can apply indexOf
because you now have an Array
object.
Object.defineProperties(Element.prototype,{
group : {
value: function (str, context) {
// str is valid css selector like :not([attr_name]) or .class_name
var t = "to_select_siblings___";
var parent = context ? context : this.parentNode;
parent.setAttribute(t, '');
var rez = document.querySelectorAll("[" + t + "] " + (context ? '' : ">") + this.nodeName + (str || "")).toArray();
parent.removeAttribute(t);
return rez;
}
},
siblings: {
value: function (str, context) {
var rez=this.group(str,context);
rez.splice(rez.indexOf(this), 1);
return rez;
}
},
nth: {
value: function(str,context){
return this.group(str,context).indexOf(this);
}
}
}
Ex
/* html */
<ul id="the_ul"> <li></li> ....<li><li>....<li></li> </ul>
/*js*/
the_ul.addEventListener("click",
function(ev){
var foo=ev.target;
foo.setAttribute("active",true);
foo.siblings().map(function(elm){elm.removeAttribute("active")});
alert("a click on li" + foo.nth());
});
Could you do something like this:
var index = Array.prototype.slice.call(element.parentElement.children).indexOf(element);
https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement
Adding a (prefixed for safety) element.getParentIndex():
Element.prototype.PREFIXgetParentIndex = function() {
return Array.prototype.indexOf.call(this.parentNode.children, this);
}
Use binary search algorithm to improve the performace when the node has large quantity siblings.
function getChildrenIndex(ele){
//IE use Element.sourceIndex
if(ele.sourceIndex){
var eles = ele.parentNode.children;
var low = 0, high = eles.length-1, mid = 0;
var esi = ele.sourceIndex, nsi;
//use binary search algorithm
while (low <= high) {
mid = (low + high) >> 1;
nsi = eles[mid].sourceIndex;
if (nsi > esi) {
high = mid - 1;
} else if (nsi < esi) {
low = mid + 1;
} else {
return mid;
}
}
}
//other browsers
var i=0;
while(ele = ele.previousElementSibling){
i++;
}
return i;
}
I had issue with text nodes, and it was showing wrong index. Here is version to fix it.
function getChildNodeIndex(elem)
{
let position = 0;
while ((elem = elem.previousSibling) != null)
{
if(elem.nodeType != Node.TEXT_NODE)
position++;
}
return position;
}
I've become fond of using indexOf
for this. Because indexOf
is on Array.prototype
and parent.children
is a NodeList
, you have to use call();
It's kind of ugly but it's a one liner and uses functions that any javascript dev should be familiar with anyhow.
var child = document.getElementById('my_element');
var parent = child.parentNode;
// The equivalent of parent.children.indexOf(child)
var index = Array.prototype.indexOf.call(parent.children, child);
I hypothesize that given an element where all of its children are ordered on the document sequentially, the fastest way should be to do a binary search, comparing the document positions of the elements. However, as introduced in the conclusion the hypothesis is rejected. The more elements you have, the greater the potential for performance. For example, if you had 256 elements, then (optimally) you would only need to check just 16 of them! For 65536, only 256! The performance grows to the power of 2! See more numbers/statistics. Visit Wikipedia
(function(constructor){
'use strict';
Object.defineProperty(constructor.prototype, 'parentIndex', {
get: function() {
var searchParent = this.parentElement;
if (!searchParent) return -1;
var searchArray = searchParent.children,
thisOffset = this.offsetTop,
stop = searchArray.length,
p = 0,
delta = 0;
while (searchArray[p] !== this) {
if (searchArray[p] > this)
stop = p + 1, p -= delta;
delta = (stop - p) >>> 1;
p += delta;
}
return p;
}
});
})(window.Element || Node);
Then, the way that you use it is by getting the 'parentIndex' property of any element. For example, check out the following demo.
(function(constructor){
'use strict';
Object.defineProperty(constructor.prototype, 'parentIndex', {
get: function() {
var searchParent = this.parentNode;
if (searchParent === null) return -1;
var childElements = searchParent.children,
lo = -1, mi, hi = childElements.length;
while (1 + lo !== hi) {
mi = (hi + lo) >> 1;
if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
hi = mi;
continue;
}
lo = mi;
}
return childElements[hi] === this ? hi : -1;
}
});
})(window.Element || Node);
output.textContent = document.body.parentIndex;
output2.textContent = document.documentElement.parentIndex;
_x000D_
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>
_x000D_
Limitations
(function(constructor){
'use strict';
Object.defineProperty(constructor.prototype, 'parentIndexBinarySearch', {
get: function() {
var searchParent = this.parentNode;
if (searchParent === null) return -1;
var childElements = searchParent.children,
lo = -1, mi, hi = childElements.length;
while (1 + lo !== hi) {
mi = (hi + lo) >> 1;
if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
hi = mi;
continue;
}
lo = mi;
}
return childElements[hi] === this ? hi : -1;
}
});
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
var child=test.children.item(99.9e+3);
var start=performance.now(), end=Math.round(Math.random());
for (var i=200 + end; i-- !== end; )
console.assert( test.children.item(
Math.round(99.9e+3+i+Math.random())).parentIndexBinarySearch );
var end=performance.now();
setTimeout(function(){
output.textContent = 'It took the binary search ' + ((end-start)*10).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
test.remove();
test = null; // free up reference
}, 125);
}, 125);
_x000D_
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>
_x000D_
(function(t){"use strict";var e=Array.prototype.lastIndexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
var child=test.children.item(99e+3);
var start=performance.now(), end=Math.round(Math.random());
for (var i=2000 + end; i-- !== end; )
console.assert( test.children.item(
Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
var end=performance.now();
setTimeout(function(){
output.textContent = 'It took the backwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
test.remove();
test = null; // free up reference
}, 125);
});
_x000D_
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>
_x000D_
(function(t){"use strict";var e=Array.prototype.indexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
var child=test.children.item(99e+3);
var start=performance.now(), end=Math.round(Math.random());
for (var i=2000 + end; i-- !== end; )
console.assert( test.children.item(
Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
var end=performance.now();
setTimeout(function(){
output.textContent = 'It took the forwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
test.remove();
test = null; // free up reference
}, 125);
});
_x000D_
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>
_x000D_
Counts the number of PreviousElementSiblings to get the parentIndex.
(function(constructor){
'use strict';
Object.defineProperty(constructor.prototype, 'parentIndexSiblingSearch', {
get: function() {
var i = 0, cur = this;
do {
cur = cur.previousElementSibling;
++i;
} while (cur !== null)
return i; //Returns 3
}
});
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
var child=test.children.item(99.95e+3);
var start=performance.now(), end=Math.round(Math.random());
for (var i=100 + end; i-- !== end; )
console.assert( test.children.item(
Math.round(99.95e+3+i+Math.random())).parentIndexSiblingSearch );
var end=performance.now();
setTimeout(function(){
output.textContent = 'It took the PreviousElementSibling search ' + ((end-start)*20).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
test.remove();
test = null; // free up reference
}, 125);
});
_x000D_
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>
_x000D_
For benchmarking what the result of the test would be if the browser optimized out the searching.
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
var start=performance.now(), end=Math.round(Math.random());
for (var i=2000 + end; i-- !== end; )
console.assert( true );
var end=performance.now();
setTimeout(function(){
output.textContent = 'It took the no search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
test.remove();
test = null; // free up reference
}, 125);
});
_x000D_
<output id=output> </output><br />
<div id=test style=visibility:hidden></div>
_x000D_
However, after viewing the results in Chrome, the results are the opposite of what was expected. The dumber forwards linear search was a surprising 187 ms, 3850%, faster than the binary search. Evidently, Chrome somehow magically outsmarted the console.assert
and optimized it away, or (more optimistically) Chrome internally uses numerical indexing system for the DOM, and this internal indexing system is exposed through the optimizations applied to Array.prototype.indexOf
when used on a HTMLCollection
object.
Source: Stackoverflow.com