JavaScript/Tutorials/Tabellen dynamisch sortieren

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

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.

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.

Beispiel
<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&#8239;000&#8239;000&#8239;000&#8239;000&#8239;000&#8239;000&#8239;000&#8239;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.

Beispiel
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. 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:

Beispiel
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 = "";
} // 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:

HTML:
<thead>
	<tr>
		<th abbr="">
			<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">
			<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>
CSS:
.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:

Beispiel
let arr = [];
for (let r = 0; r < nrows; r++) arr[r] = [];

Das Kopieren der Tabellendaten in das 2D-Array erfolgt dann so:

Beispiel
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:

Beispiel
// 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:

Beispiel
// 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:

Beispiel
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"); 
		tabletitel[sp].abbr = (tabletitel[sp].abbr==sort_info.asc) ? sort_info.desc : sort_info.asc;
	}
	else { // Sortieren 
		if (sorted > -1) {
			sortbuttons[sorted].classList.remove("sortedasc");
			sortbuttons[sorted].classList.remove("sorteddesc");
			sortbuttons[sorted].classList.add("unsorted");
			tabletitel[sorted].abbr = "";
		}
		sortbuttons[sp].classList.remove("unsorted");
		sortbuttons[sp].classList.add("sortedasc");
		sorted = sp;
		tabletitel[sp].abbr = sort_info.asc;
		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 abbr-Attribute 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:

Beispiel ansehen …
( 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 = "";
		} // 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"); 
				tabletitel[sp].abbr = (tabletitel[sp].abbr==sort_info.asc)?sort_info.desc:sort_info.asc;
			}
			else { // Sortieren 
				if (sorted > -1) {
					sortbuttons[sorted].classList.remove("sortedasc");
					sortbuttons[sorted].classList.remove("sorteddesc");
					sortbuttons[sorted].classList.add("unsorted");
					tabletitel[sorted].abbr = "";
				}
				sortbuttons[sp].classList.remove("unsorted");
				sortbuttons[sp].classList.add("sortedasc");
				sorted = sp;
				tabletitel[sp].abbr = sort_info.asc;
				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