Benutzer:Wisch/Tabellenheader

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Anlass

In verschiedenen PHP-Anwendungen zeige ich große HTML-Tabellen an und möchte dass die Spaltenüberschriften auch dann immer sichtbar sind, wenn die Tabelle für den Bildschirm zu groß ist. Die Beispiele, die ich im Internet gefunden habe, erschienen mir allesamt kompliziert. Außerdem möchte ich nicht nur Kopf- und Fußzeilen, sondern auch die Zellen der ersten Spalten flexibel fixieren können.

Die folgende Lösung arbeitet mit Kopien der entsprechenden Tabellen-Teile, die von JavaScript-Eventlister bei Bedarf absolut positioniert werden. Zunächst erkläre ich das Prinzip am Beispiel der fixierten Header. Dann folgen die Erweiterungen für Fußzeilen und Spalten. Und am Schluss steht dann ein Link auf die fertige Klasse zum Ausprobieren.

Das Prinzip

Eine HTML-Tabelle kann mit den Tags thead, tbody und tfoot in einen Kopf- Daten- und Fußzeilen-Bereich aufgeteilt werden. Wenn die Tabelle dann beim Drucken nicht auf eine Seite passt, werden die thead- und tfoot-Bereiche auf jeder Seite wiederholt. Vgl.: Aufbau einer Tabelle

Am Bildschirm im Browser funktioniert das aber so nicht. Wenn die Tabelle nicht vollständig angezeigt werden kann und nach oben gescrolled wird, soll jetzt der Bereich mit den Überschriften, der mit thead und /thead eingeschlossen ist, stehen bleiben. Technisch wird dazu dieser Bereich mit der Funktion cloneNode() kopiert und in einer neuen Tabelle über der Originaltabelle am oberen Fensterrand anzeigt. Das funktioniert auch bei Frames. Dabei geht leider das Layout der Original-Tabelle verloren, so dass wir die Zellenbreite von den Originalzellen holen müssen. Das erfolgt in einem Listener für den "resize"-Event. Ein Listener für das "scroll"-Event übernimmt die Anzeige und Positionierung.

Anmerkung: Mozilla.org rät explizit von der Verwendung der Scroll-Events ab, weil die Nebenläufikeit der Browser-Prozesse zu unerwünschten Effekten führt. Allerdings habe ich für die hier anstehende Aufgabe keine Alternative gefunden. Das Style-Attribut position: sticky funktioniert nur mit Block-Level-Elementen, nicht mit Elementen wie thead oder tr. Und ich habe in CSS auch keine Möglichkeit gefunden, ein Element horizontal im Dokument auszurichten und nur vertikal an den Viewport anzuheften.

Schritt für Schritt

Damit wir die Tabelle aus JavaScript ansprechen können, muss sie eine ID-Erhalten.

ID für Tabelle vergeben
<table id="Tabelle">

Dann erzeugen wir eine Klasse, die alle Aufgaben übernimmt.

Empfehlung: Wir verwenden in der Klasse ausschließlich private Variablen und Funktionen. Damit kommt niemand auf die Idee, Internas, die wir in einer späteren Version vielleicht ändern wollen, von außen anzusprechen. Nur Methoden, die von anderen Programmen aufgerufen werden müssen, werden (später) als öffentliche Funktionen deklariert.

Die erste Version

Das Prinzip für den Header

Die Klasse wird mit dem ID-Namen der Tabelle instantiiert. Damit können auch mehrere Tabellen in einem HTML-Dokument mit fixen Headern versehen werden.

Beginn der Klassen-Deklaration
function TabellenHeader(tabellenID) {

Zunächst müssen wir globale Variablen für die Tabelle und ihre Überschriften, das THEAD-Element, sowie für Ihre Kopien (Clones) deklarieren. Die Trennung von Deklaration und Zuweisung, ermöglicht uns später Tabellen, zu verarbeiten, bei denen z.B. nur die Fußzeile stehen bleiben soll.

Tabelle, Tabellenheader und die Clones deklarieren
var tabelle = null;
var tabTHead = null;
var tabUeberschrift = null;
var tabTHeadUeberschrift = null;

Beim Instanziieren der Klasse suchen wir nach der Tabelle über die übergebene tabellenID. Wenn es die Tabelle nicht gibt, ist die Arbeit erledigt. Damit kann man in dem Script "auf Verdacht" versuchen, für Tabellen mit bestimmten IDs Instanzen dieser Klase zu erzeugen, und braucht in dem HTML-Dokument kein zusätzliches JavaSipt.

	tabelle = document.getElementById(tabellenID);
	if (tabelle == null) return;  // KEINE TABELLE GEFUNDEN

Anschließend suchen wir das <THEAD-Element über den Tag-Namen und erstellen die Tabelle mit den kopierten Spaltenüberschriften.
tabUeberschrift wird als Clone von tabelle mit der Methode cloneNode(false) erzeugt, damit wir alle Attribute, z.B. Cellpading, übernehmen.
Für tabTHeadUeberschrift benötigen wir Kopien von allen untergeordneten Nodes, was wir mit der Methode cloneNode(true) erreichen.
Das Ganze tragen wir mit appendChild() in das Dokument ein.
Zuletzt können wir über Style-Attribute die Anzeige ab- und die absolute Positionierung einschalten.

Überschriften suchen
	
	tabTHead =  tabelle.getElementsByTagName("thead")[0];
	if (tabTHead != null) {	
		tabTHeadUeberschrift = tabTHead.cloneNode(true);
		tabUeberschrift = tabelle.cloneNode(false);

		tabUeberschrift.appendChild(tabTHeadUeberschrift);
		tabelle.parentNode.appendChild(tabUeberschrift);

		tabUeberschrift.style.position = "fixed";
		tabUeberschrift.style.tableLayout = "fixed";
		tabUeberschrift.hidden=true;

	}

Jetzt müssen wir noch die beiden Eventlistener-Funktionen resize() und scroll() erzeugen, damit der kopierte Header richtig angezeigt und positioniert wird.

Der Resize-Listener

Wenn die Fenstergröße geändert wurde, haben sich wahrscheinlich auch die Spaltenbreiten geändert.
Das Kopieren der Zellenbreiten wird hier in eine Funktion ausgelagert die sich rekursiv aufruft.
Danach muss noch die Anzeige und Position der Überschrift mit dem Listener für das "scroll"-Event überprüft werden.

Der Resize-Listener
	function resize() {
		// Header-Tabelle dimensionieren
		if (tabUeberschrift != null) {
  			copyWidth(tabTHead,tabTHeadUeberschrift);
		}
		scroll();
	}

Die Funktion copyWidth durchsucht die childNodes des <THEAD>-Elements rekursiv. Wenn ein <TH>- oder <TD>-Element gefunden wurde, wird die Zellenbreite kopiert. In clientWidth steht das Ergebnis der Zellenbreite nach dem Rendern. Dieses Attribut kann nur gelesen werden. Vor dem Setzen der Breite über das Attribut wird hier über das Attribut boxSizing eingestellt, dass die Zellenbreite und nicht etwa nur die Breite des Inhalts an width übergeben wird. Das Attribut element.style.width wird von den Browsern in verschiedenen Situationen nur als Empfehlung verstanden. Deshalb müssen explizit minWidth und maxWidth gesetzt werden.

Zellenbreite kopieren
	function copyWidth(pnode, cnode) {
		// nur "echte" Elemente berückichtigen	
		if (cnode.nodeType != 1) return;
	
		if (cnode.tagName == "TH" || cnode.tagName == "TD") {
			cnode.style.boxSizing = "border-box";
			cnode.style.minWidth = pnode.offsetWidth+"px";
			cnode.style.maxWidth = pnode.offsetWidth+"px";
			return;
		} 

		for (var i = 0; i < cnode.childNodes.length; i++) {
			if (cnode.childNodes.item(i).childNodes.length > 0) {	
 		            copyWidth(pnode.childNodes.item(i),cnode.childNodes.item(i));
			}				
		}
	}

Der Scroll-Listener

Jetzt wird es etwas kompliziert: Die Überschrift soll nur angezeigt werden, wenn man darunter noch etwas von der Tabelle sieht.
Der Scroll-Listener ermittelt die Position der Tabelle relativ zum angezeigten Fenster (Viewport) mit der Methode getBoundingClientRect(). Wenn der Anfang der Tabelle nicht mehr sichtbar ist und das Ende der Tabelle noch unterhalb der Überschrift zu sehen ist, wird die fixierte Überschrift angezeigt.

Scroll-Methode
 	function scroll() {
 		
 	// Tabellen-Position und Header-Größe bstimmen.
  		var boundingRect = tabelle.getBoundingClientRect();
  		var windowHeight = document.querySelector('html').clientHeight;  

	// Kopfzeilen behandeln
		if (tabUeberschrift != null) {
			if ( (boundingRect.top < 0) && (boundingRect.bottom > tabTHead.offsetHeight) ) {
				// Feste Kopfzeile ausrichten und anzeigen
				tabUeberschrift.style.left = boundingRect.left + "px";
				tabUeberschrift.style.top = "0px";		
				tabUeberschrift.hidden=false;
			}else{
				// Feste Kopfzeile ausblenden
				tabUeberschrift.hidden=true;
			}
		}

Initialisieren

Zum Initialisieren rufen wir die Funktion resize() direkt auf. Danach registrieren wir diese Funktion und die Funktion scroll() als Event-Listener. Die beiden Listener-Funktionen sind Methoden, die an die Instanz der Klasse gebunden sind. D.h. sie werden beim Erzeigen des Objekts registriert und haben dann Zugriff auf alle Objekt-Variablen.

Initialisieren
	resize();
	// Eventlistener registrieren
	window.addEventListener('scroll', scroll);
	window.addEventListener('resize', resize);

Damit ist das Objekt für den fixierten Tabelenheader instanziiert und registriert.

Eine abschließende } beendet die Deklaration der Klasse.

Ende der Klassen-Deklaration
    }

Verwendung

Nach dem Aufbau der Anzeige wird eine Objekt der Klasse TabellenHeader für die ID der Tabelle erzeugt, und der Rest läuft dann "wie von selbst". Wichtig ist dabei, dass das Layout der Tabelle abgeschlossen ist, damit ihre Größe ermittelt werden kann.

Ein Objekt TabellenHeader für die Tabelle mit der ID="Tabelle" erzeugen
    document.addEventListener('load', function() {
 			new TabellenHeader("Tabelle"); 
		}

Anwendung

So, wie das Script geschrieben ist, kann es in jeder HTML-Seite referenziert werden. Wenn eine Tabelle mit der ID="Tabelle" nicht existiert oder diese Tabelle keine Kopfzeilen in einem THEAD-Abschnitt hat, passiert nichts. Ansonsten ist es ziemlich egal, wie die Tabelle aussieht.

Feinheiten

Leider funktioniert die Klasse noch nicht wie gewünscht, und es sind noch etliche Feinarbeiten erforderlich.

Hintergrundfarben

Als erstes fällt auf, dass die Original-Tabelle unter der fest stehenden Überschrift durchscheint, wenn der Hintergrund der Tabelle nicht explizit gesetzt wurde. Deshalb müssen wir die Hintergrund-Farbe der Tabelle ermitteln und bei der Kopie explizit setzen. Die Suche nach der Hintergrundfarbe wird in eine Funktion ausgelagert, die wir später wiederverwenden.

 
	tabUeberschrift.style.backgroundColor = getSolidBackground(tabelle);
	tabelle.parentNode.appendChild(tabUeberschrift);

und die Funktion dazu sucht nach "oben" nach dem ersten Element mit einen nicht-transparenten Hintergrundfarbe:

Hintergrundfarbe setzen
 
    function getSolidBackground(element) {
	while (element != null) {
		var bg = window.getComputedStyle(element).backgroundColor; 
		if (bg != "transparent"	&& bg.substr(0,4) != "rgba"){
			return bg;
		}
		element = element.parentElement;
	}
	return "white";
    }

Ausdrucken

Damit die Kopie der Überschrift nicht im Ausdruck erscheint, muss sie für den Druck, und nur für den Druck, unsichtbar werden. Das lässt sich mit einer CSS-Klasse "noprint" steuern.

CSS-Stylesheet
@media print {
	.noprint {
		display: none;				
	}	
}

Diese Klasse wird dann der Header-Kopie hinzugefügt.

Klasse anfügen
	tabUeberschrift.classList.add("noprint");

Breite Tabellenränder

Wenn die Original-Tabelle einen breiten Rand hat, z.B. border="10", wird dieser auch in die kopierte Header-Tabelle übernommen. Das ist unerwünscht, weil dadurch unter der Überschrift der breite Tabellenrand angezeigt wird.

Deshalb muss ein solcher Rand für die Kopie auf 1 zurückgesetzt werden. Erschwerend kommt hinzu, dass border und cellspacing sowohl als table-Attribute im HTML wie auch als style-Attribute im CSS gesetzt sein können. Deshalb werden diese Werte beim Instanziieren des Objekts als numerische Werte ermittelt:

Werte ermitteln und speichern
	var border = 0;
	var cellSpacing = 2;
	var padding = 0;
	var tBorder = 0; // Border-Breite für kopierte Kopf- und Fußzeilen

	if (tabelle.attributes.hasOwnProperty("border")) border = parseFloat(tabelle.border);
	if (tabelle.style.border > "") border = parseFloat(tabelle.style.border);

	if (tabelle.attributes.hasOwnProperty("cellSpacing")) 
		cellSpacing = parseFloat(tabelle.cellSpacing);
	if (tabelle.style.borderSpacing > "") cellSpacing = parseFloat(tabelle.style.borderSpacing);

	if (tabelle.style.padding > "") padding = parseFloat(tabelle.style.padding);

Je nachdem, wie das Attribut gesetzt wurde, muss auch die Kopie anders behandelt werden. Deshalb wird hier eine Funktion verwendet:

	if (border > 1) {
		setBorder1(tabUeberschrift);	
	}
Funktion zum Setzen der Border
  function setBorder1(tab){
	if (tab.style.border > "") 
	    tab.style.border = "1px "+tab.style.border.substr(tab.style.border.indexOf(" "));
	else  
	if (tab.attributes.hasOwnProperty("border")) tab.border = "1";
	else tab.style.border = "1px solid black";
	tBorder = 1;
  }

Entsprechend muss die Rahmenbreite auch bei der Positionierung berücksichtigt werden:

	tabUeberschrift.style.left = (boundingRect.left + border - tBorder) + "px";

Das Caption-Element einer Tabelle

Das CAPTION-Element einer Tabelle erzeugt eine Überschrift über der Tabelle, die zu ihren Maßen gehört. Deshalb muss für die Prüfung, wann eine fixierte Kopfzeile angezeigt wird, die Postion des thead-Elements an Stelle der Position des table-Elements verwendet werden. Siehe unten.

ID-Attribut

Damit der Tabellenname, das ID-Attribut der Tabelle, auch von einem anderen Script verwendet werden kann, muss die Kopie einen eigenen Namen erhalten.

ID-Attribut ergänzen
	tabUeberschrift.id += "_Header";

Feste Fußzeilen

Manchmal haben Tabellen Summenzeilen, die man auch gerne immer auf dem Bildschirm haben möchte. Das funktioniert ganz genau so, wie bei den Überschriften, nur dass man THEAD durch TFOOT ersetzt und natürlich eigene Variablen braucht.

Komplizierter wird dann nur die Scroll-Methode:
Die Fußzeilen sollen die Kopfzeilen nicht verdecken, deshalb wird für die Prüfung zur Anzeige der Fußzeilen die Höhe der Kopfzeilen benötigt.

Scroll-Methode
 function scroll() {
     // Tabellen-Position und Header-Größe bestimmen.
  	var boundingRect = tabelle.getBoundingClientRect();
 	var windowHeight = document.querySelector('html').clientHeight;  
	var headerHeight = 0;
		
     // Kopfzeilen behandeln
	if (tabUeberschrift != null) {
		// Die Kopien sind noch nicht gerendert. Deshalb Höhe des THEAD-Elements
		var boundingRectH = tabTHead.getBoundingClientRect();
		headerHeight = boundingRectH.height;  		
		if (boundingRectH.top  < 0 
			&& (tabTLast.getBoundingClientRect().bottom > boundingRectH.height) ) {
			// Feste Kopfzeile ausrichten und anzeigen					
			tabUeberschrift.style.left = (boundingRect.left	+ border - tBorder) + "px";
			tabUeberschrift.style.top = "0px";		
			tabUeberschrift.hidden=false;
		}else{
			// Feste Kopfzeile ausblenden
			tabUeberschrift.hidden = true;
		}
	}

    // Fußzeilen behandeln
	if (tabFusszeile != null) {		
		var boundingRectF = tabTFoot.getBoundingClientRect();	
		var footerHeight = boundingRectF.height;
		if (tabTFirst.getBoundingClientRect().top < (windowHeight - footerHeight 
				- headerHeight - 1 * cellSpacing)
			&& boundingRectF.bottom > windowHeight) {
			// Feste Fußzeile ausrichten und anzeigen					
			tabFusszeile.style.left = (boundingRect.left + border - tBorder) + "px";
			tabFusszeile.style.top = (windowHeight 
						- tabFusszeile.offsetHeight + tBorder)+"px";
			tabFusszeile.hidden=false;
		}else{
			// Feste Fußzeile ausblenden
			tabFusszeile.hidden = true;
		}
	}
 }

Fixierte Spalten

Bei breiten Tabellen möchte man oft, dass die ersten Zellen zur Kennzeichnung der Zeile stehen bleiben. Allerdings gibt es dazu keine einfache Möglichkeit, die Spalten in HTML als fixiert zu kennzeichnen. Deshalb wird hier den zu fixierenden Zellen eine zusätzliche Klasse, "fts", zugewiesen. Über diese Klasse können dann alle Zellen mit der Methode classList.contains("fts") erkannt und behandelt werden. In der Implementierung wurde der Name der CSS-Klasse in eine Konstante, fixeSpaltenKlasse, die am Anfang des Programms deklariert wird, ausgelagert.

Eine weitere Komplikation entsteht durch ein Performance-Problem bei der Zuweisung zum hidden-Attribut: Wenn dieses Attribut bei jeder Zelle einzeln gesetzt wird, "ruckelt" die Anzeige. Deshalb muss eine selbst nicht anzuzeigende Tabelle erzeugt werden, welche diese Zellen als Elemente enthält, und über die die Anzeige insgesamt ein- und ausgeschaltet werden kann. Das Ganze benötigt man dann noch dreifach: für die Original-Tabelle und für die fixierten Kopf- und Fußzeilen.

Zunächst wird ein Array, zeilen, mit allen Zeilen und Zellen der Original-Tabelle angelegt. Über dieses Array kann dann einfach iteriert werden. Der Zugriff auf die Original-Zellen erfolgt dann über zeilen[i].children.item(s). In einem zweiten, zweidimensionalen, Array, kopierteZeilen, werden die Kopien der zu fixierenden Zellen gespeichert. Der Zugriff auf diese Zellen erfolgt über kopierteZellen[i][s]. In diesen Arrays stehen alle Zellen, unabhängig davon, ob sie aus der Original-Tabelle oder einer Kopie für fixierte Kopf- oder Fuß-Zeilen stammen.

Container für fixierte Spalten

Als Container muss zunächst eine leere Tabelle erzeugt werden. Diese Tabelle wird außerhalb des sichtbaren Bereichs platziert und erhält natürlich die CSS-Klasse "noprint".

Leere Tabele erzeugen
  function makeEmptyCopy(muster) {
	var tab = muster.cloneNode(false);
 	tab.id += "_fix";
	tab.classList.add("noprint");
 	tab.hidden=true;
	tab.style.position = "fixed";
 	tab.style.left="-100px";
	tab.style.top="-100px";
 	tab.style.width="10px";
	tab.style.height="10px";
 	tab.style.tableLayout = "fixed";
	return tab
  }

Als Muster verwenden wir die jeweiligen Originaltabellen. Für die ursprüngliche Tabelle ist das einfach:

Tabelle für fixierte Spalten erzeugen
    var zeilen = Array();
    var kopierteZeilen = Array();
    var fixeBodyTabelle = makeEmptyCopy(tabelle);
    var fixeSpalten = fixeSpaltenSuchen(tabelle,fixeBodyTabelle);

Zu fixierende Zellen suchen

Die Funktion fixeSpaltenSuchen wird rekursiv aufgerufen und durchsucht die Tabelle nach zu fixierenden Zellen (spalten[i]). Jede Zeile wird in dem Array zeilen gespeichert. Gleichzeitig wird ein Eintrag in kopierte Zeilen angelegt und ein <TR>-Element in der Tabelle für die Kopien erzeugt.

Für Javascript-Programme, die auf diese Zellen zugreifen, erhalten sie Referenzen aufeinander.

Die Funktion liefert true zurück, wenn mindestens eine zu fixierende Zelle gefunden wurde.

Fixe Spalten suchen
function fixeSpaltenSuchen (element,fixeTabelle) {
	var sfix = false;   // Rückgabewert
	var i = 0;
	if (element.tagName == "TR") {  // ZEILE gefunden
 		i = zeilen.length;
		zeilen[i] = element;
 		var kopie = Array();
		kopierteZeilen[i] = kopie;
		var zeile = document.createElement("tr");
 		var spalten = element.children;    
		for (i=0; i < spalten.length; i++) {
			 var sp = spalten[i];
			 if (sp.classList.contains(fixeSpaltenKlasse)) {
				sfix = true;
 				var spKopie = sp.cloneNode(true);
 				sp.THcopy = spKopie;
 				spKopie.THoriginal = sp;
 				spKopie.style.position = "fixed";    
 				spKopie.classList.add("noprint");
				spKopie.style.boxSizing = "border-box";
				spKopie.style.backgroundColor = getSolidBackground(sp);
				kopie[i] = spKopie;	
				zeile.appendChild(spKopie)
			}				
		}
		fixeTabelle.appendChild(zeile);
 	}else if(element.hasChildNodes()) {  // rekursiv weitersuchen
		var children = element.children;
		for (i=0; i < children.length; i++) {
			if (fixeSpaltenSuchen(children[i],fixeTabelle)) sfix = true;
		}	
	}	
	return sfix;
}

Anzeige aufbauen

Beim Aufbau der Anzege muss auf die richtige Reihenfolge geachtet werden: Die fixierten Zellen müssen die jeweilige Tabelle überlagern, aber die fixierten Kopf- und Fußzeilen müssen auch die fixierten Zellen der Originaltabelle überlagern. Das macht den Aufbau etwas "komplex":

Tabellen für fxierte Spalten erzeugen und Eintragen
	fixeSpalten = fixeSpaltenSuchen(tabelle,fixeBodyTabelle);	
	if (fixeSpalten) {
		var inserted = false;
		if (tabUeberschrift != null) {
			tabelle.parentElement.insertBefore(fixeBodyTabelle,tabUeberschrift);
			inserted = true;
			fixeHeadTabelle = makeEmptyCopy(tabUeberschrift);
			fixeSpaltenSuchen(tabUeberschrift,fixeHeadTabelle);
			tabelle.parentElement.appendChild(fixeHeadTabelle);
		}
		if (tabFusszeile != null) {
			if (!inserted) tabelle.parentElement.insertBefore(fixeBodyTabelle,tabFusszeile);
			inserted = true; 		
			fixeFootTabelle = makeEmptyCopy(tabFusszeile); 
			fixeSpaltenSuchen(tabFusszeile,fixeFootTabelle);
			tabelle.parentElement.appendChild(fixeFootTabelle);
		}
		if (!inserted) tabelle.parentElement.appendChild(fixeBodyTabelle);
	}

Scroll-Methode erweitern

Jetzt müssen wir nur noch die Scroll-Methode erweitern, damit die Zellen bei Bedarf angezeigt und positioniert werden. Dabei übernehmen wir auch die Größe von den Original-Zellen. Allerdings müssen die kopierten Zellen direkt aneinander grenzen, damit nicht in den Zwischenräumen aus dem cellSpacing-Attribut die Inhalte darunter stören. Dazu werden sie um das cellSpacing-Attribut der Tabelle verbreitert.

Scroll-Methode für fixierte Spalten
if (fixeSpalten) {
	var i = 0;
	var tpos = tabelle.getBoundingClientRect();
	if (tpos.left < -1 * (Number(tabelle.cellSpacing) + Number(tabelle.border))) {
		for (i=0; i < zeilen.length;i++) {
			var zeile = zeilen[i]; 
			var kopie = kopierteZeilen[i];
			var epos = zeile.getBoundingClientRect();
			var zellen = zeile.children;
			for (var z = 0; z < kopie.length; z++) {
				var zpos   = zellen.item(z).getBoundingClientRect();
				var kZelle = kopie[z];
				kZelle.style.width  = (zpos.width + Number(tabelle.cellSpacing))+"px";
				kZelle.style.height = (zpos.height)+"px";
				kZelle.style.left   = (zpos.left 
							- tpos.left 
							- Number(tabelle.border) 
							- Number(tabelle.cellSpacing))+"px";
				kZelle.style.top = (zpos.top)+"px"; 
			} // Schleife über kopierte Zellen einer Zeile			
		} // For-Schleife über zeilen / kopierteZeilen			
		
		// Fixe Zellen anzeigen				
		fixeBodyTabelle.hidden=false;
		if (tabUeberschrift != null)					
			fixeHeadTabelle.hidden = tabUeberschrift.hidden;
		if (tabFusszeile != null)					
			fixeFootTabelle.hidden = tabFusszeile.hidden;
	}else{
		// Fixe Zellen ausblenden				
		fixeBodyTabelle.hidden=true;
		if (tabUeberschrift != null)					
			fixeHeadTabelle.hidden=true;
		if (tabFusszeile != null)					
			fixeFootTabelle.hidden=true;
	}
}

Vertikale Ausrichtung der Zellinhalte

Leider verlieren die Texte in den HTML-Zellen (<td> und <th>) ihre vertikale Ausrichtung, sobald Position auf "fixed" gesetzt wird. Deshalb muss die Ausrichtung middle und bottom mit dem Attribut style.paddingTop wieder hergestellt werden. Dazu brauchen wir aber die Höhe des Inhalts, die wir nur ermitteln können, wenn die Zelle genau ein Element enthält. Wie immer benötigen wir dazu die bereits gerenderte Original-Zelle. Beim Kopieren der Zelle müssel wir den Inhalt deshalb in einen div-Abschnitt einschließen:

	sp.innerHTML = "<div>"+sp.innerHTML+"</div>";

Dann können wir den Inhalt in den kopierten Zellen wie folgt ausrichten (nur für angezeigte Zellen):

Zellinhalte vertikal ausrichten
	if (zpos.top <= windowHeight && zpos.bottom >=0){ //Nur Zellen im sichtbaren Bereich
		var oZelle = zellen.item(z);
		var vAlign = window.getComputedStyle(oZelle).verticalAlign;
		var ktHeight = oZelle.firstChild.offsetHeight; // Höhe des Zelleninhalts

		if (vAlign == "middle"){
			kZelle.style.paddingTop = ((oZelle.clientHeight - ktHeight)/2) + "px"; 
		}else if (vAlign == "bottom"){
			var paddingBottom = 
				parseFloat(window.getComputedStyle(oZelle).paddingBottom)
			kZelle.style.paddingTop = (oZelle.clientHeight - ktHeight - paddingBottom) + "px";
		}
	}

Bugs und andere Probleme

Bugs

Insbesondere der Internet Explorer führt die Scroll-Events offenbar parallel zum Aufbau der Anzeige aus. Auf der Testseite führt das dazu, dass die fixierte Fußzeile verspätet angezeigt wird. Hierzu habe ich keine Lösung gefunden.

CSS

In CSS gibt es unendlich viele Möglichkeiten, das Aussehen der Tabelle zu beeinflussen. Viele werden durch das Kopieren der Zellen automatisch korrekt verarbeitet. Aber alles, was die Position der Zellen beeinflusst ist problematisch. Beispiele:

  1. Die Tabellenattribute können als HTML-Attribute oder als Style-Attribute gesetzt werden. Ersteres ist zwar veraltet, kommt aber vor. Deshalb müssen häufig beide Varianten abgefragt werden. In der Version im Ergebnis wurden deshalb alle Referenzen auf Tabellen-Attribute, z.B. tabelle.border, durch die Konstanten border, cellSpacing und tBorder, welche beim Instanziieren der Klasse ermittelt werden, ersetzt.
  2. Ein padding-Attribut an der Tabelle sollte bei der kopierten Kopfzeile unten und an der kopierten Fußzeile oben auf 0 gesetzt werden, weil sonst unnötig Platz verschwendet wird. (Im Ergebnis unten ist das gemacht.)

Die CSS-Hover-Funktion bei fixierten Spalten

Die Zell-Kopien müssen einen nicht transparenten Hintergrund bekommen. D.h. style.backgroundColor wird explizit gesetzt, was die Änderung der Hintergrundfarbe über CSS blokiert. Beispiel:

Hover
th:hover {
        background-color: #f66;
        }

Lösung für Zellen

Die Hintergrundfarbe der kopierten Zellen für fixierte Spalten muss deshalb über die Events onMouseEnter und onMouseLeave gesetzt werden. Damit die Farb-Wahl im CSS-StyleSheet bleibt und nicht in die Javascript-Funktion verschoben wird, muss in CSS zusätzlich die Klasse hover deklariert werden:

Hover für fixierte Zellen
th:hover, th.hover {
        background-color: #f66;
        }

Jetzt können die Event-Funktionen die Hintergrundfarbe dirch die Farbe aus der CSS-Hoverklasse ersetzen und das Ganze auch wieder zurücksetzen:

Event-Funktionen für Hover auf Zell-Ebene
    function setHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        o.oldBackground  = o.style.backgroundColor;
        o.style.backgroundColor = null;
        o.classList.add('hover');
     }
    }

    function resetHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        o.classList.remove('hover');
        o.style.backgroundColor = o.oldBackground ;
     }
    }

Die Event-Handler werden am einfachsten nach der Instanziierung der TabellenHeader-Klasse wie folgt registriert:

Event-Handler registriern
    window.addEventListener('load', function() {
      var zellen = document.getElementsByClassName('fts');
      for (var i = 0; i < zellen.length; i++){
        var z = zellen[i];
        if (z.THoriginal != null) {
          // nur für Kopien für fixierte Zeilen-Label
          z.addEventListener('mouseenter',setHover,false);
          z.addEventListener('mouseleave',resetHover,false);
        }
      }

Lösung für Zeilen

Komplizierter wird es, wenn der Hover-Effekt für die ganze Zeile funktionieren soll. Beispiel:

Hover auf Zeilen-Ebene
tr:hover {
        background-color: #aaa;
        }

Die kopierten Zellen gehören nicht zur jeweiloigen Tabelle oder gar Zeile. Damit es in beide Richtungen funktioniert, von der Tabellen-Zeile zu den kopierten Zellen und umgekehrt, müssen die kopierten Zellen und die originalen Zeilen in der Tabelle Handler-Funktionen bekommen. In dem Beispiel wird angenommen, dass die Tabelle die ID='Tabelle' hat.

Event-Handler registriern
    window.addEventListener('load', function() {
      var zellen = document.getElementsByClassName('fts');
      for (var i = 0; i < zellen.length; i++){
        var z = zellen[i];
        if (z.THoriginal != null) {
          // nur für Kopien für fixierte Zeilen-Label
          z.addEventListener('mouseenter',setHover,false);
          z.addEventListener('mouseleave',resetHover,false);
        }
      }
      // Nur Zeilen der Original-Tabelle
      var zeilen = document.getElementById('Tabelle').getElementsByTagName('tr');
      for (var i = 0; i < zeilen.length; i++){
          var z = zeilen[i];
          z.addEventListener('mouseenter',copyBackground,false);
          z.addEventListener('mouseleave',resetBackground,false);
      }
     });

Auch hier muss die Pseudoklasse ':hover' um die Klasse 'hover' ergänzte werden.

Hover-Klasse auf Zeilen-Ebene
tr:hover, tr.hover {
        background-color: #aaa;
        }

Die Handler-Funktionen sorgen jetzt dafür, dass aus der Zeile die Hintergrundfarben in den kopierten Zellen explizit gesetzt und wieder zurück gesetzt werden. Die Funktionen selbst erhalten als Parameter die Referenz auf die Zeile, weil sie auch aufgerufen werden wenn die Maus über der kopierten Zelle ist. Bei den kopierten Zellen sorgen die Handler-Funktionen dafür, dass in der Zeile der Original-Zelle die Hover-Klasse gesetzt ist, und rufen dann die Handler-Funktion der Zeile auf, um den Hintergrund bei den fixierten Zellen zu behandeln.

Event-Handler für Hover auf Zeilen-Ebene
// Handler-Funktionen für die Zeilen
    function copyBackgroundF(tr){
      var nl = tr.childNodes;
      var bg = window.getComputedStyle(tr).backgroundColor;
      for (var i = 0; i < nl.length; i++){
        var cn = nl[i];
        if (cn.THcopy != null) {
          cn.THcopy.rowOldBackground = cn.THcopy.style.backgroundColor;
          cn.THcopy.style.backgroundColor = bg;
        }
      }
    }

    function resetBackgroundF(tr){  
      var nl = tr.childNodes;
      for (var i = 0; i < nl.length; i++){
        var cn = nl[i];
        if (cn.THcopy != null) {
            cn.THcopy.style.backgroundColor = cn.THcopy.rowOldBackground;
        }
      }
    }
// Aufruf der Handler-Funktionen für die Zeilen aus den Event-Handlern
    function copyBackground(event){
      copyBackgroundF(event.target);
    }

    function resetBackground(event){  
      resetBackgroundF(event.target);
    }

// Handler für die kopierten Zellen
    function setHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        var row = o.THoriginal.parentNode;
        row.classList.add('hover');
        copyBackgroundF(row);
     }
    }

    function resetHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        var row = o.THoriginal.parentNode;
        row.classList.remove('hover');
        resetBackgroundF(row);
     }
    }

Lösung für Zellen und Zeilen kombiniert

Natürlich kann beides, Zeilen- und Zellen-Hover kombiniert werden. Dann sehen die Zellen-Handler wie folgt aus:

Event-Handler für Zeilen- und Zellen-Hover
    function setHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        var row = o.THoriginal.parentNode;
        row.classList.add('hover');
        copyBackgroundF(row);
        o.oldBackground  = o.style.backgroundColor;
        o.style.backgroundColor = null;
        o.classList.add('hover');
     }
    }

    function resetHover(event){
     var o = event.target;
     if (o.THoriginal != null){
        o.classList.remove('hover');
        o.style.backgroundColor = o.oldBackground ;
        var row = o.THoriginal.parentNode;
        row.classList.remove('hover');
        resetBackgroundF(row);
     }
    }

Hinweis: In der aktuellen Version des TabellenHeaders (siehe Download unten) sind diese Funktionen integriert und müssen nur aktiviert werden.

Aktivieren der Hover-Funktion
    var th = new TabellenHeader('MeineTabelle');
    th.aktiviereHover(th.ZELLEN);
// oder:
    th.aktiviereHover(th.ZEILEN);    
// oder:
    th.aktiviereHover(th.BEIDES);

Aktualisierte Tabelleninhalte

Die Zell-Kopien bekommen nicht mit, wenn der Inhalt der Original-Zellen über ein JavaScript-Programm geändert wurde. Dabei können zwei Fälle unterschieden werden:

  1. Der normale Tabelleninhalt wird durch eine JavaScript-Routine geändert. Dann muss nur die Resize-Methode direkt aufgerufen werden. Dazu reicht eine öffentliche Wrapper-Methode.
  2. Der Inhalt einer fixierten Zelle ändert sich. Dann muss dieser Inhalt auch in die Kopien übertragen werden. Dazu benötigen wir eine neuue, öffentliche Methode refresh().
Neuberechnen der Spaltenbreiten
  this.resize = function () { resize();}

Aktualisieren der Inhalte:
Bei den Kopf- und Fußzeilen können wir hier mit einem rekursiven Funktionsaufruf, analog zum Kopieren der Spaltenbreiten arbeiten. Bei den fixierten Spalten können wir dann die bereits vorhandenen Arrays verwenden. Anschließend müssen die aktuellen Spaltenbreiten kopiert werden.

Aktualisieren
  this.refresh = function(){
		if (tabUeberschrift != null)	refresh(tabTHead,tabTHeadUeberschrift);  	
		if (tabFusszeile != null)	refresh(tabTFoot,tabTFootFusszeile);
		if (fixeSpalten) {
			for (var i=0; i < zeilen.length;i++) {
				var zeile = zeilen[i]; 
				var kopie = kopierteZeilen[i];
				var zellen = zeile.children;
				for (var z = 0; z < kopie.length; z++) {	
					kopie[z].innerHTML = zellen.item(z).innerHTML;
					var ih = zellen.item(z).innerHTML;
					var il = ih.length;
					if (il < 12) zellen.item(z).innerHTML = "<div>"+il+"</div>";
					else 
					if (ih.substr(0,5) != "<div>" || ih.substr(il-6,6) != "</div>")   
						zellen.item(z).innerHTML = "<div>"+il+"</div>";
					}
			}
		}
		resize();  	
  	}
Die Funktion für Kopf- und Fußzeilen ist wieder eine private Funktion
  	function refresh(pnode,cnode){
		// nur "echte" Elemente berückichtigen	
		if (cnode.nodeType != 1) return;
	
		if (cnode.tagName == "TH" || cnode.tagName == "TD") {
				cnode.innerHTML = pnode.innerHTML;
				return;
		} 

		for (var i=0; i < cnode.childNodes.length; i++) {
			if (cnode.childNodes.item(i).childNodes.length > 0) {	
				refresh(pnode.childNodes.item(i),cnode.childNodes.item(i));
			}				
		}
  
  	}

Jetzt muss natürlich die Referenz auf das TabellenHeader-Objekt gespeichert werden, damit später die öffentlichen Methoden des jeweiligen Objekts aufgerufen werden.

Einen global adressierbaren TabellenHeader für die Tabelle mit der ID="TabelleX" erzeugen
 	var th = null; // globale Variable
 	document.addEventListener('load', function() {
 	 	th = new TabellenHeader("TabelleX"); 
	}

Je nach Änderung des Tabelleninhalts kann dann die Ermittlung der Spaltenbreiten mit

 	th.resize();

oder die Aktualisierung der fixierten Inhalte (und anschließende Ermittlung der Spaltenbreiten) mit

 	th.refresh();

aufgerufen werden.

Ergebnis

Im Ergebnis haben wir jetzt eine Klasse, die automatisch arbeitet, wenn sie mit der ID der entsprechenden Tabelle instanziiert wird. In dem Script sind drei Namen eingetragen, zu denen entsprechende HTML-Tabellen gesucht werden.

Instanzen der Klasse TabellenHeader erzeugen
 document.addEventListener('load', function() {
 	new TabellenHeader("Tabelle"); 	
 	new TabellenHeader("Tabelle1"); 	
 	new TabellenHeader("Tabelle2"); 	
 });

Und hier sind das Programm und HTML-Tabellen zum Testen. (Solange ich noch am Optimieren bin, steht das Ganze nur auf meiner Homepage:

JavaScript-Klasse in aktueller Fassung zum Download

Tabellen zum Testen

Beispiel mit Hover