JavaScript/Tutorials/Tabellen dynamisch sortieren
Bei tabellarischen Daten muss schon beim Erstellen der Tabelle festgelegt werden, in welcher Sortierung die Daten dargestellt werden sollen. Soll durch eine Benutzeraktion die Reihenfolge geändert werden, benötigen Sie bei statischem HTML für jede Sortierung ein eigenes HTML-Dokument, bei dynamisch aus z. B. einer Datenbank erstellten Tabellen können die Daten serverseitig umsortiert werden.
Diese Sortierung kann aber auch mittels Javascript ohne Serverunterstützung im Browser durchgeführt werden. Dieser Artikel beschreibt, wie dabei vorzugehen ist.
Inhaltsverzeichnis
Die Tabelle im HTML
Der folgende HTML-Code zeigt eine Tabelle mit fünf Spalten. Die Tabelle soll nach jeder dieser Spalten durch einen Klick auf die entsprechende Spaltenüberschrift sortiert werden. Über das class
-Attribut wird dem Script mitgeteilt, dass diese Tabelle sortierbar gemacht werden soll.
Die Tabelle muss einen thead
und einen tbody
enthalten. Nur der erste tbody
wird bei der Sortierung berücksichtigt. Wenn eine caption
vorhanden ist, wird an diese eine Hinweis zur Sortierbarkeit angehängt. Ein tfoot
darf vorhanden sein.
Zahlen dürfen als Tausendertrennzeichen das schmale umbruchgeschützte Leerzeichen ( , hier verwendet), das schmale Leerzeichen ( ),das umbruchgeschützte Leerzeichen ( ) und das normale Leerzeichen enthalten.
<table class="sortierbar">
<caption>Im SI definierte Dezimalpräfixe</caption>
<thead>
<tr>
<th>Vorsilbe</th>
<th>Kurzzeichen</th>
<th>Zehnerpotenz<br>(10 hoch)</th>
<th>Wert</th>
<th>Zahl</th>
</tr>
</thead>
<tbody>
<tr>
<td>Yotta</td>
<td>Y</td>
<td>24</td>
<td>Quadrillion</td>
<td>1 000 000 000 000 000 000 000 000</td>
</tr>
<!-- ... -->
</tbody>
<tfoot>
<tr>
<td colspan=5>* Die zu Hekto(h), Deka(da), Dezi(d) und Zenti(c) gehörigen Vielfache sind keine Potenzen von 1000</td>
</tr>
</tfoot>
</table>
Zugriff auf die Tabellenelemente
Der Zugriff auf die Tabellenelemente erfolgt über die Methoden des TableObjects caption
, tHead
, tBodies
, rows
und cells
.
Der Sortierer
Zuerst muss das Script die zu sortierenden Tabellen finden. Dieses erfolgt mit der Methode querySelectorAll. Danach wird für jede Tabelle das Objekt tableSort
angelegt. Damit sichergestellt ist, dass die Tabelle auch schon angelegt ist, wird die Suche in die Funktion initTableSort
gelegt und mit addEventListener als Eventlistener zum Event DOMContentLoaded hinzugefügt.
const tableSort = function (tab) {
} // tableSort
// Alle Tabellen suchen, die sortiert werden sollen, und den Tabellensortierer starten.
const initTableSort = function () {
const sort_Table = document.querySelectorAll("table.sortierbar");
for (let i = 0; i < sort_Table.length; i++) new tableSort(sort_Table[i]);
} // initTableSort
window.addEventListener("DOMContentLoaded", initTableSort, false);
Die Sortierung soll durch einen Klick auf die Spaltenüberschrift ausgelöst werden. Dazu könnte man den th-Feldern im thead-Bereich der Tabelle einen Eventhandler für das click-Event geben. Dann wäre aber der Sortierer mit der Tastatur nicht zu bedienen. Daher setzen wir in die th-Felder
einen Button mit deren Inhalt und deren Aussehen. Um anzuzeigen, das die Tabelle sortierbar ist, bzw. nach welcher Spalte sortiert wurde, werden an die Spaltenüberschriften Pfeilsymbole als SVG angehängt. Dazu dienen die Klassen unsorted
, sortedasc
und sorteddesc
. Damit auch Screenreadern angezeigt wird, dass die Tabelle sortierbar ist, werden die Inhalte der th-Felder nur visuell versteckt, bleiben aber vorhanden. Dem Inhalt der Button wird noch ein visuell versteckter Hinweis vorangestellt. Zusätzlich wird im abbr-Attribut der th-Felder noch ein Hinweis über den Sortierzustand abgelegt und das aria-sort-Attribut auf ascending, descending oder none gesetzt. Um bei einem Doppelklick ein zweifaches Aufrufen der Sortierfunktion zu verhindern, wird die Eigenschaft detail
des Eventobjekts abgefragt.
Dem Button geben wir dann den Eventhandler für das click-Event:
const thead = tab.tHead;
let tr_in_thead, tabletitel;
if (thead) tr_in_thead = thead.rows;
if (tr_in_thead) tabletitel = tr_in_thead[0].cells;
if ( !(tabletitel && tabletitel.length > 0) ) {
console.error("Tabelle hat keinen Kopf und/oder keine Kopfzellen.");
return;
}
// Hinweistexte
const sort_info = {
asc: "Tabelle ist aufsteigend nach dieser Spalte sortiert",
desc: "Tabelle ist absteigend nach dieser Spalte sortiert",
};
const sort_hint = {
asc: "Sortiere aufsteigend nach ",
desc: "Sortiere absteigend nach ",
};
// Sortiersymbol
const sortsymbol = '<svg role="img" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 190 110"><path d="M0 0 L50 100 L100 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"/><path d="M80 100 L180 100 L130 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"/></svg>';
// Stylesheets für Button im TH
if(!document.getElementById("Stylesheet_tableSort")) {
const sortbuttonStyle = document.createElement('style');
const stylestring = '.sortbutton { width: 100%; height: 100%; border: none; background-color: transparent; font: inherit; color: inherit; text-align: inherit; padding: 0; cursor: pointer; } '
+ '.sortierbar thead th span.visually-hidden { position: absolute !important; clip: rect(1px, 1px, 1px, 1px) !important; padding: 0 !important; border: 0 !important; height: 1px !important; width: 1px !important; overflow: hidden !important; white-space: nowrap !important; } '
+ '.sortierbar caption span { font-weight: normal; font-size: .8em; } '
+ '.sortbutton svg { margin-left: .2em; height: .7em; } '
+ '.sortbutton.sortedasc svg path:last-of-type { fill: currentColor !important; } '
+ '.sortbutton.sorteddesc svg path:first-of-type { fill: currentColor!important; } '
+ '.sortbutton.sortedasc > span.visually-hidden:first-of-type { display: none; } '
+ '.sortbutton.sorteddesc > span.visually-hidden:last-of-type { display: none; } '
+ '.sortbutton.unsorted > span.visually-hidden:last-of-type { display: none; } ';
sortbuttonStyle.innerText = stylestring;
sortbuttonStyle.id = "Stylesheet_tableSort";
document.head.appendChild(sortbuttonStyle);
}
// Kopfzeile vorbereiten
const initTableHead = function(sp) {
const sortbutton = document.createElement("button");
sortbutton.type = "button";
sortbutton.className = "sortbutton unsorted";
sortbutton.addEventListener("click", function(e) { if(e.detail <= 1) tsort(sp); }, false);
sortbutton.innerHTML = "<span class='visually-hidden'>" + sort_hint.asc + "</span>"
+ "<span class='visually-hidden'>" + sort_hint.desc + "</span>"
+ tabletitel[sp].innerHTML + sortsymbol;
tabletitel[sp].innerHTML = "<span class='visually-hidden'>" + tabletitel[sp].innerHTML + "</span>";
tabletitel[sp].appendChild(sortbutton);
sortbuttons[sp] = sortbutton;
tabletitel[sp].abbr = "";
tabletitel[sp].setAttribute("aria-sort", "none");
} // initTableHead
for (let i = 0; i < tabletitel.length; i++) initTableHead(i);
Der so erzeugte Inhalt der th-Felder und das css sehen dann so aus:
<thead>
<tr>
<th abbr="" aria-sort="none">
<span class="visually-hidden">Vorsilbe</span>
<button type="button" class="sortbutton unsorted">
<span class="visually-hidden">Sortiere aufsteigend nach </span>
<span class="visually-hidden">Sortiere absteigend nach </span>
Vorsilbe
<svg role="img" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 190 110">
<path d="M0 0 L50 100 L100 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"></path>
<path d="M80 100 L180 100 L130 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"></path>
</svg>
</button>
</th>
...
<th abbr="Tabelle ist aufsteigend nach dieser Spalte sortiert" aria-sort="ascending">
<span class="visually-hidden">Wert</span>
<button type="button" class="sortbutton sortedasc">
<span class="visually-hidden">Sortiere aufsteigend nach </span>
<span class="visually-hidden">Sortiere absteigend nach </span>
Wert
<svg role="img" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 100">
<path d="M0 0 L50 100 L100 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"></path>
<path d="M80 100 L180 100 L130 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"></path>
</svg>
</button>
</th>
...
</tr>
</thead>
.sortbutton {
width: 100%;
height: 100%;
border: none;
background-color: transparent;
font: inherit;
color: inherit;
text-align: inherit;
padding: 0;
cursor: pointer;
}
.sortierbar thead th span.visually-hidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px) !important;
padding: 0 !important;
border: 0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden !important;
white-space: nowrap !important;
}
.sortierbar caption span {
font-weight: normal;
font-size: .8em;
}
.sortbutton svg {
margin-left: .2em; height: .7em;
}
.sortbutton.sortedasc svg path:last-of-type {
fill: currentColor !important;
}
.sortbutton.sorteddesc svg path:first-of-type {
fill: currentColor!important;
}
.sortbutton.sortedasc > span.visually-hidden:first-of-type {
display: none;
}
.sortbutton.sorteddesc > span.visually-hidden:last-of-type {
display: none;
}
.sortbutton.unsorted > span.visually-hidden:last-of-type {
display: none;
}
Zum Sortieren wird die Arraymethode sort verwendet. Dazu muss der Inhalt der HTML-Tabelle in ein 2D-Array kopiert werden. Zusätzlich wird auch noch die Referenz auf die Tabellenzeile in das Array kopiert. Ein 2D-Array kann man als 1D-Array ansehen, dessen Elemente wieder 1D-Arrays sind:
let arr = [];
for (let r = 0; r < nrows; r++) arr[r] = [];
Das Kopieren der Tabellendaten in das 2D-Array erfolgt dann so:
let tbdy = tab.tBodies;
if ( !(tbdy) ) {
console.error("Tabelle hat keinen tbody.");
return;
}
tbdy = tbdy[0];
const tr = tbdy.rows;
if( !(tr && tr.length > 0) ) {
console.error("Tabelle hat keine Zeilen im tbody.");
return;
}
const nrows = tr.length,
ncols = tr[0].cells.length;
// Tabelleninhalt in ein Array kopieren
for (let r = 0; r < nrows; r++) {
arr[r] = [];
for (let c = 0, cc; c < ncols; c++) {
cc = getData(tr[r].cells[c], c);
arr[r][c] = cc;
}
arr[r][ncols] = tr[r];
}
Das Auslesen der Tabellenfelder erfolgt in der Funktion:
// Tabellenfelder auslesen und auf Zahl oder String prüfen
const getData = function (ele, col) {
const val = ele.textContent;
// Tausendertrenner entfernen, und Komma durch Punkt ersetzen
const tval = val.replace(/\s/g,"").replace(",", ".");
if (!isNaN(tval) && tval.search(/[0-9]/) != -1) return tval; // Zahl
sorttype[col] = "s"; // String
return val;
} // getData
Hierbei wird geprüft, ob es sich um einen String oder eine Zahl handelt, damit später entsprechend sortiert werden kann. Eventuell vorhandene als Tausendertrenner dienende Leerzeichen werden entfernt, und eventuell verwendete Dezimalkommata werden durch Dezimalpunkte ersetzt.
Zum Sortieren eines 2D-Arrays mit der Array-Methode sort
müssen in der Vergleichsfunktion der Methode sort
die Werte aus den Zeilen-Arrays miteinander verglichen werden, die zur Spalte gehören, nach der sortiert werden soll:
// Vergleichsfunktion für Strings
const vglFkt_s = function(a, b) {
return a[sorted].localeCompare(b[sorted],doclang);
} // vglFkt_s
// Vergleichsfunktion für Zahlen
const vglFkt_n = function(a, b) {
return a[sorted] - b[sorted];
} // vglFkt_n
vglFkt_s
wird bei Textfeldern verwendet, und vglFkt_n
bei Zahlenfeldern.
In der Sortierfunktion tsort
wird dann nur noch überprüft, ob die Tabelle schon nach der gewünschten Spalte sortiert wurde, dann wird mit der Array-Methode reverse nur die Reihenfolge umgedreht, sonst wird sortiert und gespeichert, nach welcher Spalte sortiert wurde:
const tsort = function(sp) { // Der Sortierer
if (sp == sorted) { // Tabelle ist schon nach dieser Spalte sortiert, also nur Reihenfolge umdrehen
arr.reverse();
sortbuttons[sp].classList.toggle("sortedasc");
sortbuttons[sp].classList.toggle("sorteddesc");
if (tabletitel[sp].abbr==sort_info.asc) {
tabletitel[sp].abbr = sort_info.desc;
tabletitel[sp].setAttribute("aria-sort", "descending");
}
else {
tabletitel[sp].abbr = sort_info.asc;
tabletitel[sp].setAttribute("aria-sort", "ascending");
}
}
else { // Sortieren
if (sorted > -1) {
sortbuttons[sorted].classList.remove("sortedasc");
sortbuttons[sorted].classList.remove("sorteddesc");
sortbuttons[sorted].classList.add("unsorted");
tabletitel[sorted].abbr = "";
tabletitel[sorted].setAttribute("aria-sort", "none");
}
sortbuttons[sp].classList.remove("unsorted");
sortbuttons[sp].classList.add("sortedasc");
sorted = sp;
tabletitel[sp].abbr = sort_info.asc;
tabletitel[sp].setAttribute("aria-sort", "ascending");
if (sorttype[sp] == "n") arr.sort(vglFkt_n);
else arr.sort(vglFkt_s);
}
for (let r = 0; r < nrows; r++) tbdy.appendChild(arr[r][ncols]); // Sortierte Daten zurückschreiben
} // tsort
Zusätzlich werden die SVG-Symbole hinter den Spaltentiteln entsprechend eingefärbt, indem den Buttons die entsprechende Klasse zugewiesen wird. Auch werden die -Attribute abbr und aria-sort der [th] an den neuen Sortierzustand angepasst.
Beim Zurückschreiben der sortierten Daten werden die Tabellenfelder nicht einzeln zurückkopiert. Statt dessen werden die Tabellenzeilen, deren Referenzen ja auch in das 2D-Array geschrieben wurden, einfach in der neuen Reihenfolge mit appendChild in den tbody eingehängt.
Um den Seitenbesuchern zu signalisieren, dass die Tabelle sortierbar ist, wird an die caption – wenn vorhanden – noch ein Hinweis angehängt. Zusätzlich erhält die Tabelle die Klasse is_sortable, über die hier im Beispiel die Hintergrundfarbe des Tabellenkopfes geändert wird.
Das fertige Javascript
Das vollständige Script sieht jetzt so aus:
( function() {
"use strict";
const tableSort = function(tab) {
// Kopfzeile vorbereiten
const initTableHead = function(sp) {
const sortbutton = document.createElement("button");
sortbutton.type = "button";
sortbutton.className = "sortbutton unsorted";
sortbutton.addEventListener("click", function(e) { if(e.detail <= 1) tsort(sp); }, false);
sortbutton.innerHTML = "<span class='visually-hidden'>" + sort_hint.asc + "</span>"
+ "<span class='visually-hidden'>" + sort_hint.desc + "</span>"
+ tabletitel[sp].innerHTML + sortsymbol;
tabletitel[sp].innerHTML = "<span class='visually-hidden'>" + tabletitel[sp].innerHTML + "</span>";
tabletitel[sp].appendChild(sortbutton);
sortbuttons[sp] = sortbutton;
tabletitel[sp].abbr = "";
tabletitel[sp].setAttribute("aria-sort", "none");
} // initTableHead
// Tabellenfelder auslesen und auf Zahl oder String prüfen
const getData = function (ele, col) {
const val = ele.textContent;
// Tausendertrenner entfernen, und Komma durch Punkt ersetzen
const tval = val.replace(/\s/g,"").replace(",", ".");
if (!isNaN(tval) && tval.search(/[0-9]/) != -1) return tval; // Zahl
sorttype[col] = "s"; // String
return val;
} // getData
// Vergleichsfunktion für Strings
const vglFkt_s = function(a, b) {
return a[sorted].localeCompare(b[sorted],"de");
} // vglFkt_s
// Vergleichsfunktion für Zahlen
const vglFkt_n = function(a, b) {
return a[sorted] - b[sorted];
} // vglFkt_n
// Der Sortierer
const tsort = function(sp) {
if (sp == sorted) { // Tabelle ist schon nach dieser Spalte sortiert, also nur Reihenfolge umdrehen
arr.reverse();
sortbuttons[sp].classList.toggle("sortedasc");
sortbuttons[sp].classList.toggle("sorteddesc");
if(tabletitel[sp].abbr==sort_info.asc) {
tabletitel[sp].abbr = sort_info.desc;
tabletitel[sp].setAttribute("aria-sort", "descending");
}
else {
tabletitel[sp].abbr = sort_info.asc;
tabletitel[sp].setAttribute("aria-sort", "ascending");
}
}
else { // Sortieren
if (sorted > -1) {
sortbuttons[sorted].classList.remove("sortedasc");
sortbuttons[sorted].classList.remove("sorteddesc");
sortbuttons[sorted].classList.add("unsorted");
tabletitel[sorted].abbr = "";
tabletitel[sorted].setAttribute("aria-sort", "none");
}
sortbuttons[sp].classList.remove("unsorted");
sortbuttons[sp].classList.add("sortedasc");
sorted = sp;
tabletitel[sp].abbr = sort_info.asc;
tabletitel[sp].setAttribute("aria-sort", "ascending");
if(sorttype[sp] == "n") arr.sort(vglFkt_n);
else arr.sort(vglFkt_s);
}
for (let r = 0; r < nrows; r++) tbdy.appendChild(arr[r][ncols]); // Sortierte Daten zurückschreiben
} // tsort
// Tabellenelemente ermitteln
const thead = tab.tHead;
let tr_in_thead, tabletitel;
if (thead) tr_in_thead = thead.rows;
if (tr_in_thead) tabletitel = tr_in_thead[0].cells;
if ( !(tabletitel && tabletitel.length > 0) ) {
console.error("Tabelle hat keinen Kopf und/oder keine Kopfzellen.");
return;
}
let tbdy = tab.tBodies;
if ( !(tbdy) ) {
console.error("Tabelle hat keinen tbody.");
return;
}
tbdy = tbdy[0];
const tr = tbdy.rows;
if ( !(tr && tr.length > 0) ) {
console.error("Tabelle hat keine Zeilen im tbody.");
return;
}
const nrows = tr.length,
ncols = tr[0].cells.length;
// Einige Variablen
let arr = [],
sorted = -1,
sortbuttons = [],
sorttype = [];
// Hinweistexte
const sort_info = {
asc: "Tabelle ist aufsteigend nach dieser Spalte sortiert",
desc: "Tabelle ist absteigend nach dieser Spalte sortiert",
};
const sort_hint = {
asc: "Sortiere aufsteigend nach ",
desc: "Sortiere absteigend nach ",
};
// Sortiersymbol
const sortsymbol = '<svg role="img" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 190 110"><path d="M0 0 L50 100 L100 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"/><path d="M80 100 L180 100 L130 0 Z" style="stroke:currentColor;fill:transparent;stroke-width:10;"/></svg>';
// Stylesheets für Button im TH
if(!document.getElementById("Stylesheet_tableSort")) {
const sortbuttonStyle = document.createElement('style');
const stylestring = '.sortbutton { width: 100%; height: 100%; border: none; background-color: transparent; font: inherit; color: inherit; text-align: inherit; padding: 0; cursor: pointer; } '
+ '.sortierbar thead th span.visually-hidden { position: absolute !important; clip: rect(1px, 1px, 1px, 1px) !important; padding: 0 !important; border: 0 !important; height: 1px !important; width: 1px !important; overflow: hidden !important; white-space: nowrap !important; } '
+ '.sortierbar caption span { font-weight: normal; font-size: .8em; } '
+ '.sortbutton svg { margin-left: .2em; height: .7em; } '
+ '.sortbutton.sortedasc svg path:last-of-type { fill: currentColor !important; } '
+ '.sortbutton.sorteddesc svg path:first-of-type { fill: currentColor!important; } '
+ '.sortbutton.sortedasc > span.visually-hidden:first-of-type { display: none; } '
+ '.sortbutton.sorteddesc > span.visually-hidden:last-of-type { display: none; } '
+ '.sortbutton.unsorted > span.visually-hidden:last-of-type { display: none; } ';
sortbuttonStyle.innerText = stylestring;
sortbuttonStyle.id = "Stylesheet_tableSort";
document.head.appendChild(sortbuttonStyle);
}
// Kopfzeile vorbereiten
for (let i = 0; i < tabletitel.length; i++) initTableHead(i);
// Array mit Info, wie Spalte zu sortieren ist, vorbelegen
for (let c = 0; c < ncols; c++) sorttype[c] = "n";
// Tabelleninhalt in ein Array kopieren
for (let r = 0; r < nrows; r++) {
arr[r] = [];
for (let c = 0, cc; c < ncols; c++) {
cc = getData(tr[r].cells[c],c);
arr[r][c] = cc;
// tr[r].cells[c].innerHTML += "<br>"+cc+"<br>"+sorttype[c]; // zum Debuggen
}
arr[r][ncols] = tr[r];
}
// Tabelle die Klasse "is_sortable" geben
tab.classList.add("is_sortable");
// An caption Hinweis anhängen
const caption = tab.caption;
if(caption) caption.innerHTML += "<br><span>Ein Klick auf die Spaltenüberschrift sortiert die Tabelle.</span>";
} // tableSort
// Alle Tabellen suchen, die sortiert werden sollen, und den Tabellensortierer starten.
const initTableSort = function() {
const sort_Table = document.querySelectorAll("table.sortierbar");
for (let i = 0; i < sort_Table.length; i++) new tableSort(sort_Table[i]);
} // initTable
if (window.addEventListener) window.addEventListener("DOMContentLoaded", initTableSort, false); // nicht im IE8
})();
Fazit
Dieser Tabellensortierer liefert in vielen Fällen schon zufriedenstellende Ergebnisse. Allerdings sind z. B. Datums- und Zeitangaben noch nicht berücksichtigt. Als Tausendertrenner bei Zahlen werden nur normale und geschützte, normal breite und schmale Leerzeichen unterstützt. Der im deutschen Spachraum gerne verwendete Punkt wird dagegen nicht unterstützt. Auch können Sie noch nicht wählen, nach welchen Spalten sortiert werden soll. Einen deutlich umfangreicheren Tabellensortierer finden Sie unter https://www.j-berkemeier.de/TableSort.html.
Weblinks
- j-berkemeier.de: TableSort