Quel plaisir que de travailler avec un langage script, particulièrement le JavaScript. On combine des objets et des méthodes sophistiqués avec de simples chaines de caractère comme un poisson dans l’eau! Tout est dans le guillemet. Il faudrait, après le copier-coller, donner un prix Nobel à celui qui a instauré les guillemets informatiques, et les entités HTML tant qu’à faire! D’ailleurs, Tim Berners-Lee n’a toujours pas son prix Nobel, une honte ;-)
Caractères accentués payés cher
Cet exemple utilise la méthode « localeCompare
». C’est la seule façon native d’espérer régler le problème des caractères accentués. C’est-à-dire d’une part à la merci de l’usager et de l’autre, une pression et un temps d’exécution supplémentaire déjà long en JavaScript. De l’ordre de 5x avec IE11 et Chrome. Déjà 20x avec mon Firefox 36 et jusqu’à 200x sur Android! C’est encore pire avec Safari 5. Dire que la vitesse d’un tri a historiquement été une préoccupation. On peut se consoler sachant que les nouveaux navigateurs sons beaucoup plus performants avec le « localeCompare
».
Voilà donc un tri des colonnes d’un tableau dynamique sans jQuery. « Un petit bout de code qui fait beaucoup », avec moult techniques tant modernes que classiques. Un très bon exemple, syntaxe des plus concises qui n’a rien à envier au jQuery. C’est déjà la moitié du slogan de jQuery « write less, do more » ça. Aucun code, il suffit d’ajouter une classe « avectri » à vos tableaux, le script les détecte et fait le travail, comme si Google était chez vous! Tout est entièrement automatisé et dynamique. Génération des flèches, ajout des événements, et ce pour un nombre "illimité" de tableaux dans une même page HTML.
Pour six bonnes raisons :
- Premièrement, parce qu’il est temps de se passer de jQuery ...quand c’est possible.
- Parce que la plupart des solutions complètes et exemple en ligne utilisent aussi la librairie jQuery.
- Parce que les nouveaux sélecteurs CSS3 sont vraiment plus efficaces, d’ailleurs supporté par jQuery.
NOTE : Le tableau doit être équilibré, sans colspan
ni rowspan
pour fonctionner.
CSS et HTML minimum
Rien de compliqué, en CSS deux classes pour les flèches et une table HTML avec la classe « averti ». Notez qu’il faut spécifier les colonnes numériques dans l’entête, pour le tri. Avec l’attribut « data-type
.
<style> /* Classe obligatoire pour les flèches */ .flecheDesc {...} .flecheAsc {...} </style> <table class="avectri"> <thead> <tr> <th>Entête 1</th> <th>Entête 2</th> <th class="selection" data-tri="1" data-type="num">Entête 3</th> </tr> </thead> <tbody> </tbody> </table>
C’est tout! Avec les entêtes et un <tbody> pour contenir les lignes et colonnes du tableau, sans plus. Optionnel puisque le navigateur ajoute toujours automatiquement le <tbody> s’il est omis. La classe « selection » permet de forcer un tri par défaut. data-type="num"
pour un tri numérique et data-tri="1"
pour un tri ascendant par défaut.
JavaScript
On commence par initialiser le tableau. C’est-à-dire boucler [].forEach
tous les tableaux de la classe « avectri » getElementsByClassName("avectri")
du document tout en bouclant chaque entête de chaque tableau querySelectorAll("th")
.
// Boucler les tableaux de classe « avectri ». [].forEach.call( document.getElementsByClassName("avectri")... // On récupère la première ligne du tableau, surment le <thead>. // pour boucleer les entêtes du <thead>, c’est à dire les <th>. var oEntete = oTableau.getElementsByTagName("tr")[0]; [].forEach.call( oEntete.querySelectorAll("th")...
Tout ça pour ajouter à chaque entête la flèche innerHTML
, les attributs setAttribute
et l’événement addEventListener
:
oTh.innerHTML += "<span class=\"flecheAsc\"></span>"; oTh.setAttribute("data-tri", "1"); oTh.setAttribute("data-pos", nI); oTh.addEventListener("click", twTriTableau, false); // Test pour forcer le tri par défaut if (oTh.className=="selection") { oTh.click(); } nI++;
La flèche est en fait une classe CSS dans un <span>. L’attribut data-tri
servira de valeur booléenne pour identifier la direction du tri. Quant à l’attribut data-pos
, c’est un index, pour palier à une lacune en pure JavaScript. En effet aucun Index n’est disponible pour les items ou les nœuds du « NodeList »! Voilà une bonne façon de palier au problème, c’est dynamique any way! Quant à l’événement, il permet de capturer le clic sur l’entête qui appellera la fonction « twTriTableau ».
La fonction appelé après un clic sur un entête.
Cette fonction ne fait pas que trier la colonne. Elle commence par supprimer la classe « selection » des entêtes. Pour ensuite l’ajouter à la nouvelle entête sélectionnée. Puis elle récupère getAttribute
la position de l’index « data-pos » de la colonne sélectionnée ainsi que la valeur booléenne « data-tri » qui indique la direction du tri courant. Pour évaluer la nouvelle direction et sauvegarder la nouvelle valeur dans l’attribut setAttribute
.
// Boucle chaque entête pour suppromer la classe [].forEach.call( this.parentNode.querySelectorAll("th"), function(oTh) {oTh.classList.remove("selection");}); // Ajoute la classe pour l’entête sélectionné. this.className = "selection"; // Des colonne numérique, Récupération de la position de l’index et de la direction var sFonctionTri = (this.getAttribute("data-type")=="num") ? "compareNombres" : "compareLocale"; var nIndex = this.getAttribute("data-pos"); var nBoolDir = this.getAttribute("data-tri"); // Évaluation et assignation de la nouvelle direction this.setAttribute("data-tri", (nBoolDir=="0") ? "1" : "0");
Contrairement au jQuery, il n’y a pas de « switch » natif en JavaScript. J’ai dû faire plus d’une solution pour ajuster la flèche parce que j’avais l’ambition d’utiliser les nouvelles méthodes. Par exemple :
// Mauvais if (this.getAttribute("data-tri")==0) { this.querySelector("span").classList.remove("flecheAsc"); this.querySelector("span").classList.add("flecheDesc"); } else { this.querySelector("span").classList.remove("flecheDesc"); this.querySelector("span").classList.add("flecheAsc"); }
Pour réaliser que la bon viel attribut « className » était encore mieux, étant donnée qu’il n’y a qu’une seule classe.
// Mieux if (this.getAttribute("data-tri")==0) { this.querySelector("span").className = "flecheAsc"); } else { this.querySelector("span").className = "flecheDesc"); } // Encore mieux, dans sa plus simple expression : this.querySelector("span").className = (nBoolDir=="0") ? "flecheAsc" : "flecheDesc");
Et enfin le tri
L’idée est toute simple, récupérer les données du <tbody> du tableau sélectionné pour les intégrer dans une matrice « aColonne ».
// Construit la matrice // Récupère le tableau (tbody) var oTbody = this.parentNode.parentNode.parentNode.getElementsByTagName("tbody")[0]; var oLigne = oTbody.rows; var nNbrLigne = oLigne.length; var aColonne = new Array(), i, j, oCel; for(i = 0; i < nNbrLigne; i++) { oCel = oLigne[i].cells; aColonne[i] = new Array(); for(j = 0; j < oCel.length; j++){ aColonne[i][j] = oCel[j].innerHTML; } }
On se retrouve avec une matrice « aColonne » qu’il suffit de trier selon la colonne sélectionnée (nIndex de l’attribut « data-pos »), les colonne numérique avec la fonction numérique, sinon, c’est localCompare pour prendre en charge les caractères accentués :
// Récupère le numéro de la colonne var nIndex = this.getAttribute("data-pos"); // Récupère le type de tri (numérique ou par défaut « local ») var sFonctionTri = (this.getAttribute("data-type")=="num") ? "compareNombres" : "compareLocale"; // Tri aColonne.sort(eval(sFonctionTri)); // Tri numérique function compareNombres(a, b) {return a[nIndex-1] - b[nIndex-1];} // Tri local (pour support utf-8) function compareLocale(a, b) {return a[nIndex-1].localeCompare(b[nIndex-1]);} // Renverse la matrice dans le cas d’un tri descendant if (nBoolDir==0) aColonne.reverse();
Le dernière ligne permet d’inverser l’ordre de la matrice dans le cas d’un tri est descendant « nBoolDir = 0 »
Selon vjeux, le tri JavaScript peut être optimisé, jusqu’à 5 fois plus performant en forçant le navigateur à ignorer la conversion toString
. En réalité mes chiffres sont plus sobre et any way localeCompare
fait sauter la pile à lui seul! Enfin voilà l’exemple :
// Tri de la matrice // Plus rapide en bloquant la conversion toString var save = Object.prototype.toString; Object.prototype.toString = function () { return String.fromCharCode(this.key); }; aColonne.sort(eval(sFonctionTri)); Object.prototype.toString = save; function compareNombres(a, b) {return a[nIndex-1] - b[nIndex-1];} function compareLocale(a, b) {return a[nIndex-1].localeCompare(b[nIndex-1])}
Et finalement on boucle la matrice pour réintégrer les valeurs dans le tableau HTML.
// Construit les colonne du nouveau tableau for(i = 0; i < nNbrLigne; i++){ aColonne[i] = "<td>"+aColonne[i].join("</td><td>")+"</td>"; } // assigne les lignes au tableau oTbody.innerHTML = "<tr>"+aColonne.join("</tr><tr>")+"</tr>";
Code complet stylisé
Tableau HTML
<table class="avectri"> <thead> <tr> <th>Entête 1</th> <th>Entête 2</th> <th data-tri="1" class="selection" data-type="num">Entête 3</th> </tr> </thead> <tbody> <tr><td>...</td><td>...</td><td>...</td></tr> ... </tbody> </table>
CSS du tableau
<style> /* Classe obligatoire pour les flèches */ .flecheDesc { width: 0; height: 0; float:right; margin: 10px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid black; } .flecheAsc { width: 0; height: 0; float:right; margin: 10px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid black; } /* Classe optionnelle pour le style */ .tableau {width:100%;table-layout: fixed;border-collapse: collapse;} .tableau td {padding:.3rem} .zebre tbody tr:nth-child(odd) {background-color: #d6d3ce;border-bottom:1px solid #ccc;color:#444;} .zebre tbody tr:nth-child(even) {background-color: #c6c3bd;border-bottom:1px solid #ccc;color:#444;} .zebre tbody tr:hover:nth-child(odd) {background-color: #999690;color:#ffffff;} .zebre tbody tr:hover:nth-child(even) {background-color: #999690;color:#ffffff;} .avectri th {text-align:center;padding:5px 0 0 5px;vertical-align: middle;background-color:#999690;color:#444;cursor:pointer; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; } .avectri th.selection {background-color:#5d625c;color:#fff;} .avectri th.selection .flecheDesc {border-bottom-color: white;} .avectri th.selection .flecheAsc {border-top-color: white;} .zebre tbody td:nth-child(3) {text-align:center;} </style>
Javascript
<script> // Tri dynamique de tableau HTML // Auteur : Copyright © 2015 - Django Blais // Source : http://trucsweb.com/tutoriels/Javascript/tableau-tri/ // Sous licence du MIT. function twInitTableau() { // Initialise chaque tableau de classe « avectri » [].forEach.call( document.getElementsByClassName("avectri"), function(oTableau) { var oEntete = oTableau.getElementsByTagName("tr")[0]; var nI = 1; // Ajoute à chaque entête (th) la capture du clic // Un picto flèche, et deux variable data-* // - Le sens du tri (0 ou 1) // - Le numéro de la colonne [].forEach.call( oEntete.querySelectorAll("th"), function(oTh) { oTh.addEventListener("click", twTriTableau, false); oTh.setAttribute("data-pos", nI); if(oTh.getAttribute("data-tri")=="1") { oTh.innerHTML += "<span class=\"flecheDesc\"></span>"; } else { oTh.setAttribute("data-tri", "0"); oTh.innerHTML += "<span class=\"flecheAsc\"></span>"; } // Tri par défaut if (oTh.className=="selection") { oTh.click(); } nI++; }); }); } function twInit() { twInitTableau(); } function twPret(maFonction) { if (document.readyState != "loading"){ maFonction(); } else { document.addEventListener("DOMContentLoaded", maFonction); } } twPret(twInit); function twTriTableau() { // Ajuste le tri var nBoolDir = this.getAttribute("data-tri"); this.setAttribute("data-tri", (nBoolDir=="0") ? "1" : "0"); // Supprime la classe « selection » de chaque colonne. [].forEach.call( this.parentNode.querySelectorAll("th"), function(oTh) {oTh.classList.remove("selection");}); // Ajoute la classe « selection » à la colonne cliquée. this.className = "selection"; // Ajuste la flèche this.querySelector("span").className = (nBoolDir=="0") ? "flecheAsc" : "flecheDesc"; // Construit la matrice // Récupère le tableau (tbody) var oTbody = this.parentNode.parentNode.parentNode.getElementsByTagName("tbody")[0]; var oLigne = oTbody.rows; var nNbrLigne = oLigne.length; var aColonne = new Array(), i, j, oCel; for(i = 0; i < nNbrLigne; i++) { oCel = oLigne[i].cells; aColonne[i] = new Array(); for(j = 0; j < oCel.length; j++){ aColonne[i][j] = oCel[j].innerHTML; } } // Trier la matrice (array) // Récupère le numéro de la colonne var nIndex = this.getAttribute("data-pos"); // Récupère le type de tri (numérique ou par défaut « local ») var sFonctionTri = (this.getAttribute("data-type")=="num") ? "compareNombres" : "compareLocale"; // Tri aColonne.sort(eval(sFonctionTri)); // Tri numérique function compareNombres(a, b) {return a[nIndex-1] - b[nIndex-1];} // Tri local (pour support utf-8) function compareLocale(a, b) {return a[nIndex-1].localeCompare(b[nIndex-1]);} // Renverse la matrice dans le cas d’un tri descendant if (nBoolDir==0) aColonne.reverse(); // Construit les colonne du nouveau tableau for(i = 0; i < nNbrLigne; i++){ aColonne[i] = "<td>"+aColonne[i].join("</td><td>")+"</td>"; } // assigne les lignes au tableau oTbody.innerHTML = "<tr>"+aColonne.join("</tr><tr>")+"</tr>"; } </script>
Nom | Ville | Âge |
---|---|---|
Luc | Montréal | 28 |
Pierre | Éloi | -20 |
Gaston | Rivière-du-Loup | 21 |
Paul | Cacouna | 26 |
Bertrand | Les Boules | -12 |
Annette | Saint-Arsène | 32 |
Ti-Guy | Rimouski | 470 |
Django | L’Isle-Verte | 49 |
Claude | Rimouski | 39 |
Alternatives
« Data Grids » en JavaScript
Modules pour jQuery
- Animated table sort
- Flexigrid for jQuery
- Le fameux DataTables - Data Table jQuery plugin
- Ingrid
- jTPS - Datatable jQuery Plugin
- TableSorter
Prototypes
Autres