Benutzer:Wisch/Tabellenheader
Inhaltsverzeichnis
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.
<table id="Tabelle">
Dann erzeugen wir eine Klasse, die alle Aufgaben übernimmt.
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.
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.
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.
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.
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.
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.
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.
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.
}
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.
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:
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.
@media print {
.noprint {
display: none;
}
}
Diese Klasse wird dann der Header-Kopie hinzugefügt.
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:
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);
}
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.
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.
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".
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:
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.
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":
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.
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):
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:
- 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.
- 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:
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:
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:
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:
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:
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.
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.
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.
// 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:
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.
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:
- 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.
- 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().
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.
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();
}
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.
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.
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: