Obtenir l'index des nœuds enfants

125

En javascript pur (c'est-à-dire sans extensions telles que jQuery, etc.), existe-t-il un moyen de déterminer l'index d'un nœud enfant à l'intérieur de son nœud parent sans itérer et comparer tous les nœuds enfants?

Par exemple,

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

Existe-t-il une meilleure façon de déterminer l'indice de l'enfant?

Tous les travailleurs sont essentiels
la source
2
Pardon, suis-je un idiot complet? Il y a beaucoup de réponses apparemment apprises ici, mais pour obtenir tous les nœuds enfants, vous n'avez pas besoin de faire parent.childNodes, plutôt que parent.children?. Ce dernier ne répertorie que les Elements, en excluant en particulier les Textnœuds ... Certaines des réponses ici, par exemple en utilisant previousSibling, sont basées sur l'utilisation de tous les nœuds enfants, tandis que d'autres ne sont dérangées qu'avec des enfants qui sont Elements ... (!)
mike rongeur
@mikerodent Je ne me souviens pas de mon objectif lorsque j'ai posé cette question au départ, mais c'est un détail clé dont je n'étais pas au courant. À moins que vous ne soyez prudent, .childNodesdevrait certainement être utilisé à la place .children. Les 2 premières réponses publiées donneront des résultats différents comme vous l'avez souligné.
Tous les travailleurs sont essentiels
Lorsque vous prévoyez d'effectuer des milliers de recherches sur plus de 1000 nœuds, attachez des informations au nœud (par exemple via child.dataset). Le but serait de convertir un algorithme O (n) ou O (n ^ 2) en un algorithme O (1). L'inconvénient est que si des nœuds sont ajoutés et supprimés régulièrement, les informations de position associées attachées aux nœuds devront également être mises à jour, ce qui pourrait ne pas entraîner de gains de performances. L'itération occasionnelle n'est pas un gros problème (par exemple, le gestionnaire de clics) mais des itérations répétées sont problématiques (par exemple, mousemove).
CubicleSoft

Réponses:

122

vous pouvez utiliser la previousSiblingpropriété pour parcourir les frères et sœurs jusqu'à ce que vous reveniez nullet comptiez le nombre de frères que vous avez rencontrés:

var i = 0;
while( (child = child.previousSibling) != null ) 
  i++;
//at the end i will contain the index.

Veuillez noter que dans des langages comme Java, il existe une getPreviousSibling()fonction, mais dans JS, cela est devenu une propriété - previousSibling.

Liv
la source
2
Oui. Vous avez cependant laissé un getPreviousSibling () dans le texte.
Tim Down
7
cette approche nécessite le même nombre d'itérations pour déterminer l'index enfant, donc je ne peux pas voir comment ce serait beaucoup plus rapide.
Michael
31
Version une ligne:for (var i=0; (node=node.previousSibling); i++);
Scott Miles
2
@sfarbota Javascript ne connaît pas la portée des blocs, il isera donc accessible.
A1rPun
3
@nepdev Ce serait à cause des différences entre .previousSiblinget .previousElementSibling. Le premier atteint les nœuds de texte, le second non.
abluejelly
133

J'ai commencé à utiliser indexOfpour cela. Parce que indexOfc'est allumé Array.prototypeet parent.childrenest un NodeList, vous devez utiliser call();C'est un peu moche, mais c'est une ligne unique et utilise des fonctions que tout développeur javascript devrait connaître de toute façon.

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);
KhalilRavanna
la source
7
var index = [] .indexOf.call (child.parentNode.children, child);
cuixiping
23
Fwiw, using []crée une instance Array chaque fois que vous exécutez ce code, ce qui est moins efficace pour la mémoire et GC que pour l'utilisation Array.prototype.
Scott Miles
@ScottMiles Puis-je demander d'expliquer un peu plus ce que vous avez dit? Ne []nettoie- t-il pas la mémoire en tant que valeur de garbage?
mrReiha
14
Pour évaluer [].indexOfle moteur, il faut créer une instance de tableau juste pour accéder à l' indexOfimplémentation sur le prototype. L'instance elle-même reste inutilisée (elle fait du GC, ce n'est pas une fuite, c'est juste une perte de cycles). Array.prototype.indexOfaccède directement à cette implémentation sans allouer d'instance anonyme. La différence sera négligeable dans presque toutes les circonstances, donc franchement, cela ne vaut peut-être pas la peine de s'en préoccuper.
Scott Miles
3
Méfiez-vous des erreurs dans IE! Internet Explorer 6, 7 et 8 le prend en charge, mais inclut à tort des nœuds de commentaire. Source " developer.mozilla.org/en-US/docs/Web/API/ParentNode
...
102

ES6:

Array.from(element.parentNode.children).indexOf(element)

Explication:

  • element.parentNode.children→ Renvoie les frères de element, y compris cet élément.

  • Array.from→ Convertit le constructeur de childrenen Arrayobjet

  • indexOf→ Vous pouvez postuler indexOfcar vous avez maintenant un Arrayobjet.

Abdennour TOUMI
la source
2
Solution la plus élégante, de loin :)
Amit Saxena
1
Mais uniquement disponible à partir de Chrome 45 et Firefox 32, et pas du tout disponible sur Internet Explorer.
Peter
Internet Explorer toujours en vie? Just Jock .. Ok donc vous avez besoin d'un polyfill pour faire des Array.fromtravaux sur Internet Explorer
Abdennour TOUMI
9
Selon MDN, appeler Array.from () creates a new Array instance from an array-like or iterable object. Créer une nouvelle instance de tableau juste pour trouver un index peut ne pas être efficace en mémoire ou en GC, selon la fréquence de l'opération, auquel cas l'itération, comme expliqué dans la réponse acceptée, serait plus idéal.
TheDarkIn1978
1
@ TheDarkIn1978 Je suis conscient qu'il y a un compromis entre l'élégance du code et les performances des applications 👍🏻
Abdennour TOUMI
38

ES: plus court

[...element.parentNode.children].indexOf(element);

L'opérateur de diffusion est un raccourci pour cela

philipp
la source
C'est un opérateur intéressant.
Tous les travailleurs sont essentiels
Quelle est la différence entre e.parentElement.childNodeset e.parentNode.children?
mesqueeb
4
childNodescomprend également des nœuds de texte
philipp
Avec Typescript vous obtenez Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
ZenVentzi
9

Ajout d'un élément (préfixé pour la sécurité) .getParentIndex ():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}
Mikemaccana
la source
1
Il y a une raison à la douleur du développement Web: les développeurs préfixés. Pourquoi ne pas le faire if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }? Quoi qu'il en soit, si cela est jamais implémenté dans la norme à l'avenir, il sera probablement implémenté comme un getter comme element.parentIndex. Donc, je dirais que la meilleure approche seraitif(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}
Jack Giffin
3
Parce que le futur getParentIndex()peut avoir une signature différente de votre implémentation.
mikemaccana le
7

Je suppose que, étant donné un élément où tous ses enfants sont ordonnés sur le document de manière séquentielle, le moyen le plus rapide devrait être de faire une recherche binaire, en comparant les positions des éléments dans le document. Cependant, comme introduit dans la conclusion, l'hypothèse est rejetée. Plus vous avez d'éléments, plus le potentiel de performance est grand. Par exemple, si vous aviez 256 éléments, alors (de manière optimale) vous n'auriez besoin de vérifier que 16 d'entre eux! Pour 65536, seulement 256! La performance atteint la puissance de 2! Voir plus de chiffres / statistiques. Visitez 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);

Ensuite, la façon dont vous l'utilisez consiste à obtenir la propriété 'parentIndex' de n'importe quel élément. Par exemple, consultez la démo suivante.

(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;
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>

Limites

  • Cette implémentation de la solution ne fonctionnera pas dans IE8 et les versions antérieures.

Recherche Binary VS Linear Sur 200 mille éléments (peut planter certains navigateurs mobiles, ATTENTION!):

  • Dans ce test, nous verrons combien de temps il faut à une recherche linéaire pour trouver l'élément du milieu VS une recherche binaire. Pourquoi l'élément du milieu? Parce qu'il se trouve à l'emplacement moyen de tous les autres emplacements, il représente donc au mieux tous les emplacements possibles.

Recherche binaire

Recherche linéaire vers l'arrière (`lastIndexOf`)

Recherche linéaire avant (`indexOf`)

PrécédentElementSibling Counter Search

Compte le nombre de PreviousElementSiblings pour obtenir le parentIndex.

Pas de recherche

Pour évaluer ce que serait le résultat du test si le navigateur optimisait la recherche.

La Conculsion

Cependant, après avoir consulté les résultats dans Chrome, les résultats sont à l'opposé de ce qui était attendu. La recherche linéaire plus stupide vers l'avant était une surprenante 187 ms, 3850%, plus rapide que la recherche binaire. De toute évidence, Chrome a en quelque sorte déjoué console.assertet optimisé par magie , ou (plus optimiste) Chrome utilise en interne un système d'indexation numérique pour le DOM, et ce système d'indexation interne est exposé à travers les optimisations appliquées Array.prototype.indexOflorsqu'il est utilisé sur un HTMLCollectionobjet.

Jack Giffin
la source
Efficace, mais peu pratique.
applemonkey496
3

Utilisez un algorithme de recherche binaire pour améliorer les performances lorsque le nœud a des frères et sœurs en grande quantité.

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;
}
cuixiping
la source
Ça ne marche pas. Je suis obligé de souligner que la version IE et la version "autre navigateur" calculeront des résultats différents. La technique des «autres navigateurs» fonctionne comme prévu, obtenant la nième position sous le nœud parent, mais la technique IE «récupère la position ordinale de l'objet, dans l'ordre source, car l'objet apparaît dans toute la collection du document» ( msdn.microsoft .com / en-gb / library / ie / ms534635 (v = vs.85) .aspx ). Par exemple, j'ai obtenu 126 en utilisant la technique "IE", puis 4 en utilisant l'autre.
Christopher Bull
1

J'ai eu un problème avec les nœuds de texte et l'index était incorrect. Voici la version pour y remédier.

function getChildNodeIndex(elem)
{   
    let position = 0;
    while ((elem = elem.previousSibling) != null)
    {
        if(elem.nodeType != Node.TEXT_NODE)
            position++;
    }

    return position;
}
John wantstoknow
la source
0
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());
     });
Bortunac
la source
1
Pouvez-vous expliquer pourquoi vous étendez Element.prototype? Les fonctions semblent utiles mais je ne sais pas ce que font ces fonctions (même si les noms sont évidents).
A1rPun
@ extend Element.prototype la raison est la similitude ... 4 ex elemen.children, element.parentNode etc ... donc de la même manière que vous abordez element.siblings .... la méthode group est un peu compliquée car je veux étendre un peu l'approche fraternelle aux elelemts semblables par le même nodeType et ayant les mêmes attributs même n'ayant pas le même ancêtre
bortunac
Je sais ce qu'est l'extension de prototype mais j'aime savoir comment votre code est utilisé. el.group.value()??. Mon premier commentaire est là pour améliorer la qualité de votre réponse.
A1rPun
les méthodes de groupe et de frères et sœurs retournent un tableau avec des éléments dom fondés .. .... merci pour votre commentaire et pour la raison du commentaire
bortunac
Très élégant, mais aussi très lent.
Jack Giffin
-1
<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>
ofir_aghai
la source
Vous n'avez pas besoin de jQuery ici.
Vitaly Zdanevich