mit absoluter CSS-Positionierung ersetzen. Da positionierte div-Elemente von JavaScript als Layer behandelt werden, ändert sich am Skript-Bereich überhaupt nichts. Nur der Layer selbst wird anders geschrieben:
Hier steht ganz viel Text
Lösung zu 9: Die Positionsangaben im Style-Attribut der div-Tags werden (genau wie die in einem layer-Tag) immer relativ zu demjenigen Dokument ausgewertet, in dem das div-Element sich befindet. Die Beispielseite enthält vier Dokumente: Das Dokument des BraunglasContainers liegt innerhalb des AltglasContainerDokuments, dieses wiederum liegt innerhalb des RecyclingHofs, welcher sich schließlich im Hauptdokument befindet. Um die Position des BraunglasContainers zu bestimmen, müssen Sie die Koordinaten zusammenzählen: Seine
204
Lösungen linke Kante ist 0px entfernt von der linken Kante des BraunglasContainers, diese Kante ist 100px rechts von der des RecyclingHofs, welche 300px von der linken Fensterkante entfernt liegt. Die resultierende x-Koordinate ist also 400px. Die y-Koordinate ergibt, wie Sie leicht nachprüfen können, 310px. Lösung zu 10: Zum Beispiel mit document.RecyclingHof.document.AltglasContainer.document. BraunglasContainer.document.Bierflasche.src = "milch.jpg";
Wenn Ihnen Übung 7 Spaß gemacht hat, können Sie jetzt die 255 anderen Möglichkeiten aufzählen, wie man eine Referenz auf das Bild erhalten kann. Lösung zu 11: Zuerst verteilen wir ein paar Pflanzen:
z-index:20">
z-index:30">
z-index:10">
z-index:40">
z-index:20">
Wie Sie sehen, war ich faul und habe nur zwei Grafiken erstellt, die ich in verschiedene Größen skaliere. Genauso mache ich es bei den Fischen:
Jetzt müssen wir die Fische animieren. Dazu brauchen wir JavaScript.
205
9 Drei Einführungen in DHTML Es gibt zahlreiche Herangehensweisen für Animations-Effekte. Hier ein ganz einfacher Vorschlag. Im Laufe dieses und des nächsten Kapitels werden Ihnen noch elegantere Lösungen begegnen. Erst einmal halten wir die aktuelle Bewegungsrichtung unserer Fische fest: var richtung_fisch1 = "rechts"; var richtung_fisch2 = "links";
Dann brauchen wir eine Funktion, die die Fische um einen Schritt vorwärts bewegt und gegebenenfalls die Richtung wechselt: function bewegeFische(){ if (richtung_fisch1=="rechts") { // Fisch schwimmt nach rechts if (document.fisch1.left<800) // 3 Pixel vorwärts document.fisch1.left+=3; else { // wenden richtung_fisch1 = "links"; document.fisch1.document.f1.src="fischli.gif"; } } else { // Fisch schwimmt nach links if (document.fisch1.left>100) document.fisch1.left-=3; else { // wenden richtung_fisch1 = "rechts"; document.fisch1.document.f1.src="fischre.gif"; } } // Dasselbe fuer den zweiten Fisch: if (richtung_fisch2=="rechts") { if (document.fisch2.left<600) // dieser Fisch ist etwas langsamer: document.fisch2.left+=2; else { richtung_fisch2 = "links"; document.fisch2.document.f2.src="fischli.gif"; } } else { if (document.fisch2.left>0) document.fisch2.left-=2; else { richtung_fisch2 = "rechts"; document.fisch2.document.f2.src="fischre.gif"; } } }
206
Lösungen Mit setInterval rufen wir diese Funktion alle 50 Millisekunden auf: setInterval("bewegeFische()",50);
Für flüssige Bewegungen sollten Sie stets einen Zeitabstand von 30-50 Millisekunden wählen. Ein gewisses Ruckeln ist dabei leider immer noch zu erkennen. Niedrigere Werte können die Browser aber (zumindest auf Windows-Betriebssystemen) nicht verarbeiten. Nur auf MacIntosh und Unix/Linux sind wirklich weiche Bewegungsabläufe möglich. Lösung zu 12: Wir schreiben erst einmal die Layer. So sieht der erste aus:
Quadrat 1
Um die Stapelreihenfolge festzulegen, habe ich ihm neben einer Position und einer Hintergrundfarbe einen festen z-index-Wert zugewiesen. Lässt man bei einer Layer-Definition die z-index-Angabe weg, dann gibt Netscape dem Layer automatisch den z-index-Wert des zuletzt erzeugten Layers. Bei gleichem zindex-Wert liegt der zuletzt definierte Layer oben. Die anderen Quadrate sehen ähnlich aus, ich ändere einfach Namen, Position, Farbe und z-index:
Quadrat 2
Quadrat 3
Quadrat 4
207
9 Drei Einführungen in DHTML
Quadrat 5
Als Nächstes brauche ich ein Formular zur Steuerung der Stapelreihenfolge:
Jetzt fehlen nur noch die JavaScript-Funktionen. Diese sind ganz einfach: function hellhoch() { for (i=1;i<=5;i++) { document.layers["q"+i].zIndex=i; } } function dunkelhoch() { for (i=1;i<=5;i++) { document.layers["q"+i].zIndex=6-i; } } function mischen() { for (i=1;i<=5;i++) { document.layers["q"+i].zIndex=Math.random()*10; } }
Ich mache mir hier die Tatsache zu Nutze, dass beim Verweisen auf Layer mittels document.layers["LayerID"]
208
Lösungen der Layername als String angegeben wird. Deshalb kann ich innerhalb der eckigen Klammern String-Operationen ausführen und Variablen einsetzen. Mit anderen Schreibweisen wie document.LayerID
wäre das nicht möglich. Zusammen mit den strategisch günstig gewählten Layernamen erhalte ich so angenehm schlanke Funktionen. Lösung zu 13: Nachdem die CSS-Angaben im style-Bereich untergebracht sind, sieht das Menü wie folgt aus:
Dazu kommt der einfache Button, der die JavaScript-Funktion aufruft:
Die Funktion will erst einmal wissen, ob das Menü im Moment aus- oder eingeklappt ist. Deshalb definieren wir eine globale Variable, die den aktuellen Menüzustand speichert: var zustand = "versteckt";
Die Funktion selbst sollte Ihnen keine allzu großen Schwierigkeiten bereiten, sie entspricht weitgehend den Funktionen zum Bewegen der Fische im Aquarium. Mein Vorschlag sieht diesmal so aus: function menuEinAus() { if (zustand == "versteckt") { document.menu.clip.bottom+=5; if (document.menu.clip.bottom>150) { zustand = "sichtbar"; return; } } else { document.menu.clip.bottom-=5; if (document.menu.clip.bottom<1) {
209
9 Drei Einführungen in DHTML zustand = "versteckt"; return; } } setTimeout("menuEinAus()",30); }
Diesmal habe ich die return-Anweisung eingesetzt: Wenn die Bewegung an ihrem Ziel angelangt ist, wird damit die Abarbeitung der Funktion beendet. Lösung zu 14: Sie brauchen zwei ineinander verschachtelte Layer hintergrund und inhalt:
Die Style-Definitionen aus Übung 13 muss dazu um die folgenden Einträge ergänzt werden: #hintergrund { position:absolute; left:250; top:30; clip:rect(0px 500px 500px 0px); layer-background-image:url(semitrans.gif) } #inhalt { position:absolute; left:10; top:10 }
In der JavaScript-Funktion, die den Inhalt von inhalt wechselt, können Sie entweder mit src oder mit load arbeiten. Die erste Variante sieht so aus: function lade(url) { document.hintergrund.document.inhalt.src = url; }
Wir nehmen aber besser load, um dadurch die Layer-Breite bestimmen zu können. Außerdem wollen wir, dass nach der Auswahl eines Menüpunktes das Menü wieder zuklappt. Deshalb wird noch menuEinAus() aufgerufen: function lade(url) { document.hintergrund.document.inhalt.load(url,480); menuEinAus(); }
Anschließend müssen nur noch die Menü-Einträge angepasst werden, damit sie die Funktion lade() aufrufen:
Lösung zu 15: Mit document.all["Satie"]
oder document.all.Satie
oder window.Satie
oder Satie
Ob sich das Element in einem div befindet (positioniert oder nicht), ist dem Internet Explorer ziemlich egal. Lösung zu 16: Was passiert ist Folgendes: Dem Objekt document.all.Satie wird eine neue Eigenschaft verpasst, nämlich visibility, und dieser Eigenschaft wird als Wert der String "hidden" zugewiesen. Sonst passiert nichts. Um das Satie-Element unsichtbar zu machen, muss die Eigenschaft visi bility seines style-Objekts geändert werden: document.all["Satie"].style.visibility = "hidden";
Lösung zu 17:
... erster Absatz ...
... zweiter Absatz ...
... dritter Absatz ...
211
9 Drei Einführungen in DHTML Lösung zu 18: Zuerst brauchen wir ein Element, in dem der Text steht. Anfangs hat dieser eine sehr kleine Schriftgröße:
der Zoom-Text
Wenn die Seite fertig geladen ist, wird eine JavaScript-Funktion aufgerufen, die die Schrift vergrößert:
Um nicht jedesmal die aktuelle Schriftgröße auslesen zu müssen, legen wir eine Variable an, die den derzeitigen Wert enthält: var schriftGroesse = 2;
Bei jedem Funktionsaufruf wird die Schrift um 5 Pixel vergrößert. Wenn 200px erreicht sind, bricht die Funktion ab, andernfalls ruft sie sich mit 30 Millisekunden Verzögerung erneut auf. function zoom() { schriftGroesse+=5; zoomDiv.style.fontSize = schriftGroesse+"px"; if (schriftGroesse<200) setTimeout("zoom()",30); }
Wenn Sie das Beispiel ausprobiert haben, konnten Sie vermutlich (wenn Sie nicht gerade eine Kinoleinwand als Bildschirm verwenden) feststellen, wie der Internet Explorer das Dokument ständig neu rendert: Er fügt zum Beispiel wenn nötig Zeilenumbrüche ein, um den Text im Fenster unterzubringen. Lösung zu 19: Drei Textbereiche sollen eingezoomt werden:
Kein Angst!
Es ist nur
DHTML...
212
Lösungen Für die zoom-Funktion brauchen wir diesmal neben der Größen-Variable auch eine Variable, die den aktuellen Grauwert speichert. Am Anfang ist er 255, die Schrift also weiß: var schriftGroesse = 2; var farbe = 255;
Jetzt könnte man einfach drei Funktionen schreiben, von denen die erste den ersten div-Bereich einzoomt, wenn sie fertig ist, die zweite aufruft, die den zweiten einzoomt, und so weiter. Es geht aber kompakter, indem das aktuelle div-Element ebenfalls in einer Variablen mitgezählt wird. Die Referenzierung auf das Element geht dann so: document.all["zoomDiv"+divZaehler]
Hier die vollständige Funktion: var divZaehler = 1; function zoom(){ schriftGroesse += 5; // jeder Absatz wird in 20 Schritten gezoomt, deshalb // machen wir die Schrift bei jedem Schritt um 255/20 // dunkler, so dass wir am Schluss bei 0 ankommen: farbe -= 255/20; var el = document.all["zoomDiv"+divZaehler]; el.style.fontSize = schriftGroesse+"px"; el.style.color = "rgb("+farbe+","+farbe+","+farbe+")"; if (schriftGroesse>100) { // ab in die nächste Runde divZaehler++; schriftGroesse = 2; farbe = 255; } if (divZaehler<4) setTimeout("zoom()",30); }
Lösung zu 20: Zuerst das Formular mit dem Eingabefeld:
Dann das graue Rechteck:
213
9 Drei Einführungen in DHTML (Beachten Sie, dass bei Internet Explorer keine clip-Angabe nötig ist, um die Fläche des div zu erweitern, width und height reichen aus. Das Attribut layer-background-color gibt es natürlich auch nicht.) Schließlich die verblüffend simple JavaScript-Funktion: function schreib() { document.all.rechteck.innerHTML = document.forms['formular'].eingabe.value; }
Sie können den »HTML-Editor«-Effekt übrigens noch verbessern, indem Sie den Button weglassen und stattdessen im textarea-Tag einen onkeyup-Handler einsetzen:
Damit wirkt sich jede Eingabe ins Formularfeld automatisch im Anzeige-Rechteck aus. Lösung zu 21: Ich wende den Filter auf das body-Element an:
gar nicht fade <script> var z = 0; function fadein(){ z+=5; document.body.style.filter = "alpha(opacity:"+z+")"; if (z<100) setTimeout("fadein()",50); } ...der Seiteninhalt...
Für einfache Effekte wie Ein-, Aus- und Überblenden bietet Microsoft vorgefertigte dynamische Filter an. Das folgende Beispiel zeigt einen eleganten, mit Hilfe des blendTrans-Filters realisierten Mouseover-Effekt:
214
Lösungen <script> function rollover() { document.all.bild.filters.blendTrans.Apply(); document.all.bild.src = "bildOver.gif"; document.all.bild.filters.blendTrans.Play(); } function rollout(){ document.all.bild.filters.blendTrans.Apply(); document.all.bild.src = "bild.gif"; document.all.bild.filters.blendTrans.Play(); }
Dynamische Filter funktionieren derzeit nur mit Internet Explorer auf Windows, und auch hier nicht in allen Versionen: Manche IE 4 ignorieren sie völlig, und selbst IE 5.5 versteht viele Filtereffekte nicht mehr (- dafür kennt er ein paar Neue). Lösung zu 22: Im HTML-Quelltext der Hauptseite steht ein Element, in dem das externe Dokument angezeigt werden soll:
Dann brauchen wir einen Iframe, der den Seiteninhalt zwischenspeichert. Mit geeigneten Style-Angaben wird er unsichtbar: <iframe id="puffer" style="width:0px; height:0px">
Um die externe Seite einzubinden, wird die Funktion lade() aufgerufen:
eine andere Seite
Diese Funktion tut nichts anderes, als die übergebene Adresse in den Iframe zu laden:
215
9 Drei Einführungen in DHTML function lade(url) { puffer.location.href = url; }
Schließlich brauchen wir noch eine Funktion, die aufgerufen wird, wenn der Iframe-Inhalt fertig geladen ist. Dann nämlich kann der Inhalt des Iframe-body ausgelesen und in den anzeige-div kopiert werden: function fertig(){ document.all.anzeige.innerHTML = puffer.document.body.innerHTML; }
Was noch fehlt, ist nur ein entsprechender Eintrag im body-Tag der externen Seite:
Das war's. In Version 5 des Internet Explorers gibt es noch einen besseren Weg, der auf einem DHTML-Behavior beruht. Behaviors werden derzeit von Netscape 6 und der Windows-Version des IE5 unterstützt (das Format ist allerdings zwischen beiden sehr unterschiedlich). Es handelt sich dabei um externe Komponenten, die das Standardverhalten bestimmter HTML-Elemente festlegen. Bei Microsoft gibt eine Reihe von Default-Behaviors, die jedoch keine Extra-Datei verlangen. Dazu gehört auch das Download-Behavior. Die Syntax ist ganz einfach: Wir nehmen irgendein Element und verpassen ihm das Behavior:
(Bei gewöhnlichen Behaviors würde man dort, wo hier #default#download steht, den URL der externen Datei eintragen.) Damit besitzt das Element automatisch die Methode startDownload(), der zwei Parameter übergeben werden: Eine einzulesende (Text-)Datei und eine Funktion, die aufgerufen wird, wenn die Datei fertig gelesen ist. Dieser Funktion wird dann der Datei-Inhalt automatisch als String übergeben. Für unser Beispiel sähe das so aus: schaufel.startDownload('eins.html', fertig); function fertig(text) { document.all.anzeige.innerHTML = text; }
Lösung zu 23: Siehe hierzu Abbildung 9.3.
216
Lösungen Abbildung 9.3: Der DOM-Baum von Übung 23
html
body
head
title
p
h2
“Wo”
“Wo kämen wir hin...”
“...wenn jeder sagte: ”
b
,
br
“Wo kämen wir hin?”
“und keiner ginge, um zu sehen, wohin wir kämen, wenn wir gingen?” Lösung zu 24: Beide Ausdrücke zeigen auf den TextNode »Wo kämen wir hin...«. document.childNodes[0].lastChild entspricht auf der Beispielseite document.body, und »Wo kämen wir hin...« ist das erste Kind des ersten Kindes (das h2-Element) des body. Das erste Kind eines Elements erreicht man wahlweise mit firstChild oder childNodes[0]. Lösung zu 25: Sie ahnten es: Auch dieser Ausdruck verweist auf »Wo kämen wir hin...«. Schauen wir uns die einzelnen Schritte an: Wir starten mit document.body.previousSibling
beim head-Element, machen mit lastChild.parentNode
von dort einen nutzlosen Abstecher zum title und zurück. Über parentNode
landen wir beim Eltern-Element des head, also bei html, von da an geht es mit
217
9 Drei Einführungen in DHTML childNodes[1].childNodes[1]
über den body zum TextNode »...wenn jeder sagte: ». Dessen vorangehender Node, previousSibling
ist das h2-Element. lastChild
führt uns schließlich zu dessen einzigem Kind: »Wo kämen wir hin...«. Lösung zu 26: Die Methoden zum Anhängen und Entfernen von Nodes beziehen sich immer auf die Kinder des Elements, von dem aus Sie aufgerufen werden. (Daher das »child« in appendChild und removeChild.) Der Inhalt der Überschrift ist aber ein Kind des h2-Elements, nicht von document.body. Korrekt müsste es also heißen: var el = document.body.childNodes[0].firstChild; document.body.childNodes[0].removeChild(el);
oder: var el = document.body.childNodes[0].firstChild; el.parentNode.removeChild(el);
Lösung zu 27: function ordnung() { // In einem Array speichern wir die zu überprüfenden // Tags: var tags = new Array("H1","H2","H3","H4","H5"); // Dann brauchen wir eine Variable, die die Überschriften // zählt: var zaehler = 1; // Jetzt durchsuchen wir die Kinder von body // und ändern den Inhalt aller Überschriften: for (i=0; i<document.body.childNodes.length; i++){ kind = document.body.childNodes[i]; for (j=0; j
218
Lösungen Lösung zu 28: function ordnung(){ // Zuerst die globalen Variablen, angefangen mit dem // Überschriften-Zähler und dem Array für die gesuchten // Tags: var tags = new Array("H1","H2","H3","H4","H5"); var zaehler = 1; // Wir brauchen außerdem einen Array für die Texte der // Überschriften, aus denen wir das Inhaltsverzeichnis // aufbauen: var inhalt = new Array(); // Als Nächstes durchsuchen wir die Kinder des body // nach Überschriften, geben ihnen IDs, benennen ihren // Inhalt um und sammeln ihn im Array inhalt: for (i=0; i<document.body.childNodes.length; i++){ kind = document.body.childNodes[i]; for (j=0; j
219
9 Drei Einführungen in DHTML // hinter jeden Link einen Zeilenumbruch: var punktBr=document.createElement("br"); inhaltEl.appendChild(punktBr); } }
Lösung zu 29: // Neues Fenster öffnen: popup=open("","ausgabeFenster"); // pre-Element für den Baum erzeugen: var pd = popup.document; var baum = pd.createElement("PRE"); pd.body.appendChild(baum); // unterste DOM-Ebene schreiben: for (var i=0; i<document.childNodes.length; i++){ var kind = document.childNodes[i]; var el = pd.createElement("DIV"); if (document.childNodes[i].nodeType==1){ var inh = pd.createTextNode("+ "+kind.tagName); el.appendChild(inh); baum.appendChild(el); holeKinder("",document.childNodes[i]); } } function holeKinder(plusminus, element){ for (var i=0; i<element.childNodes.length; i++){ // Erst einmal den Baum um eine Zeile weitermalen. // Die Reihe von Strichen und Plus-Zeichen, die // links von denen der aktuellen DOM-Ebene stehen, // wurde als Parameter plusminus übergeben. var el = pd.createElement("DIV"); var inh = pd.createTextNode(plusminus+"|"); el.appendChild(inh); baum.appendChild(el); // Dann schreiben wir die Inhalte der TextNodes und // die Tag-Namen der Elemente dieser Ebene: var kind = element.childNodes[i]; var el = pd.createElement("DIV"); if (element.childNodes[i].nodeType==1){ var neuInh = plusminus + "+--+ " + kind.tagName; var inh = pd.createTextNode(neuInh); el.appendChild(inh);
220
Lösungen baum.appendChild(el); // nächsten Rekursionsschritt starten: if (element.childNodes.length>i+1) holeKinder(plusminus+"| ", kind); else holeKinder(plusminus+" ", kind); } else if (element.childNodes[i].nodeType==3){ var neuInh = plusminus + "+-- " + kind.nodeValue; var inh = pd.createTextNode(neuInh); el.appendChild(inh); baum.appendChild(el); } } }
Auf der CD finden Sie eine Version dieses Skripts, die Sie als Bookmark speichern können. Wenn Sie dieses Bookmark auf einer beliebigen Seite aufrufen, malt es Ihnen den DOM-Baum des Dokuments in ein neues Fenster. Lösung zu 30: Eine eigentliche Lösung zu dieser Übung gibt es natürlich nicht. Falls Sie jedoch bei Ihren Versuchen auf nichts Interessantes gestoßen sind, hier ein paar Anregungen: 왘 Sehen Sie sich die Unterschiede zwischen der DOM-Darstellung von Net-
scape 6 und Internet Explorer 5 an. Beispielsweise werden Sie entdecken, dass bei IE5 der im title angegebene Titel kein Kind des title-Elements ist. Auch die Behandlung von Zeilenumbrüchen zwischen Tags und in TextNodes ist unterschiedlich: Netscape macht (genau wie IE5 auf MacIntosh) aus jedem Zeilenumbruch zwischen einzelnen Elementen im body einen TextNode. 왘 Nehmen Sie einmal unter die Lupe, was die Browser aus falschem HTML-
Quellcode machen, etwa aus:
auweia
Sie werden feststellen: Um die Fehler auszugleichen, werden intern Elemente erzeugt, die im Quelltext gar nicht vorkommen. 왘 Wenn Sie nicht regelmäßig HTML-Spezifikationen lesen, dürften Sie auch
beim Betrachten einer table in der Baum-Ansicht überrascht sein: Laut W3C steht innerhalb des table-Elements stets ein tbody-Element. Wurde es vom Autor der Seite weggelassen, fügen es die Browser von alleine ein.
221
10
DHTML: Cross-Browser-Strategien Von Wolfgang Schwarz, [email protected]
10.1
Grundlagen
Kennen Sie das? Sie haben eine Webseite entwickelt, mit Ihrem Lieblingsbrowser getestet, vielleicht noch ein paar Fehler behoben und Bugs umschifft – schließlich läuft alles wie gewünscht. Dann kommen Sie (oder Ihr Arbeitgeber) mit einem anderen Browser vorbei, und die Seite fliegt Ihnen um die Ohren: Das Layout ist verwüstet, es hagelt Fehlermeldungen, wenn Sie Pech haben, stürzt auch gleich der Browser ab. Diese Erfahrung ist so etwas wie der Initiationsritus für Cross-Browser-DHTML. Das Problem heißt Inkompatibilität: Die völlig unterschiedlichen Objektmodelle der verschiedenen Browser zusammen mit den zahlreichen Bugs, die sich in ihre Implementierung eingeschlichen haben, machen es unmöglich, dass eine Seite, die für einen bestimmten Browser entwickelt wurde, ohne erheblichen Aufwand auch auf einem anderen zum Laufen zu bringen ist. In diesem Kapitel erfahren Sie, wie man DHTML-Seiten von Anfang an so entwickelt, dass sie später mit mehr als nur einer Browserversion funktionieren. Ich werde Ihnen unterschiedliche Herangehensweisen vorstellen, mit denen Sie im Übungsteil ausgiebig experimentieren können. Am Ende des Kapitels dürfte Ihnen der Gedanke an Cross-Browser-DHTML keinen Schrecken mehr einjagen. Auf jeden Fall sollten Sie, bevor Sie dieses Kapitel durcharbeiten, Kapitel 9 gründlich gelesen haben. Es ist nicht möglich, die verschiedenen Objektmodelle von Microsoft, Netscape und W3C unter ein Dach zu bringen, ohne diese Objektmodelle überhaupt zu kennen. Vorüberlegungen zur Zielgruppe Wir wollen, dass unsere DHTML-Seiten möglichst mit allen Browsern auf allen Plattformen ohne Abstürze und Augenschmerzen betrachtet werden können. Nun gibt es, wie Sie vermutlich wissen, ziemlich viele Betriebssysteme, und mindestens ebenso viele Browserversionen. Wir müssen also erst einmal sortieren. Da wären als Erstes all jene Browser, die gar nichts können. Zu diesen Puristen gehören Textbrowser wie Lynx, aber auch neueste Netscape- oder Internet Explorer-Versionen, bei denen JavaScript ausgeschaltet ist.
Browser-Gruppen
223
10 DHTML: Cross-Browser-Strategien Eine zweite Gruppe bilden diejenigen Browser, die zwar ein wenig JavaScript und vielleicht CSS beherrschen, aber kein DHTML. Hierunter fallen zum Beispiel die Versionen 2 und 3 von Netscape, der Internet Explorer 3 und Opera. Die dritte Gruppe schließlich bilden die zumindest prinzipiell DHTML-fähigen Browser. Dazu gehören natürlich Netscape und Internet Explorer ab Version 4, aber auch zum Beispiel einige AOL-Browser der Versionen 3, 4 und 5. Allerdings gibt es von diesen AOL-Browsern unzählige Versionen, die zwar fast alle auf Internet Explorer basieren, aber in ihrer JavaScript-Funktionalität in ziemlich unberechenbarer Weise eingeschränkt sind. Da AOL (genau wie Opera) praktisch keine Dokumentation bereitstellt, müssen Sie, wenn Sie für AOL-Browser entwickeln, mindestens 5 von ihnen auf verschiedenen Betriebssystemen installiert haben, um damit jede Anweisung zu testen. In der Praxis macht das natürlich kein Mensch. Man verbannt AOL stattdessen einfach in Gruppe 2 und lässt ihn erst gar nicht an die DHTML-Skripte heran. Es bleiben an DHTML-fähigen Browsern also Internet Explorer ab 4, Netscape 4 und W3C-konforme Browser wie Netscape 6. Diese Liste wird in absehbarer Zeit noch ziemlich wachsen, denn bei vielen kleineren Browsern (einschließlich Opera) ist eine DOM-Unterstützung geplant. Von den DHTML-Browsern gibt es nun wiederum unterschiedliche Versionen je nach Betriebssystem. Während sich Netscape unter Windows, MacIntosh und Unix relativ ähnlich verhält, sind die MacIntosh-Versionen von Internet Explorer 4 und 5 in vieler Hinsicht ganz andere Browser als ihre Windows-Kollegen – insbesondere enthalten sie eine Reihe zusätzlicher Bugs, die oft nur schwer zu umgehen sind. Wenn Sie also keinen Mac zum Testen haben, dann schicken Sie sicherheitshalber auch diese Browser in Gruppe 2. Browser-Check Soviel zur Theorie. Jetzt brauchen wir ein Skript, das den Browser des Besuchers überprüft und in seine Gruppe einweist, wo er dann fachgerecht behandelt werden kann. BrowserErkennung
Die beiden Möglichkeiten zur Browser-Erkennung haben Sie bereits in Kapitel 3 kennen gelernt: Entweder man liest die Eigenschaften des navigatorObjekts aus oder testet die Verfügbarkeit bestimmter Objekte. Zum Beispiel gibt es das Objekt document.layers nur im Objektmodell von Netscape 4, Sie können deshalb davon ausgehen, dass eine mit if (document.layers)
eingeleitete Anweisung nur von diesem Browser ausgeführt wird. Ebenso testet man üblicherweise auf das Objektmodell von Internet Explorer mit if (document.all)
und auf das W3C DOM mit if (document.getElementById)
224
Grundlagen Aber Vorsicht! Das sind keine verlässlichen Browsertests. Es kann jederzeit ein neuer Browser erscheinen, der zwar das überprüfte Objekt kennt, das dazugehörige Objektmodell aber nur unvollständig umsetzt. So wird aller Voraussicht nach der neue MacIntosh-Browser iCab neben dem W3C DOM auch einen Teil des Microsoft-Objektmodells verstehen, darunter die Collection document.all. Noch gefährlicher ist Opera 4 bzw. 5, bei dem einige DOM-Methoden wie document.getElementById() zwar bereits vorhanden sind, aber davon kaum etwas funktioniert. Idealerweise würde man bei bei jedem einzelnen Objekt und jeder Methode vor der Verwendung überprüfen, ob es vorhanden ist und richtig funktioniert. Das ist in der Praxis aber selten machbar. Es bleibt deshalb nichts anderes übrig, als den Browsermarkt im Auge zu behalten und eventuelle Störenfriede einzeln abzufangen und in ihre Gruppe einzuweisen. Hierbei ist die userAgentEigenschaft des navigator-Objekts ganz brauchbar: if (document.all && navigator.userAgent.toLowerCase().indexOf("mac")>-1)
erkennt die MacIntosh-Ausgabe des Internet Explorers, if (navigator.userAgent.toLowerCase().indexOf("aol")>-1)
testet auf AOL-Browser, if (navigator.userAgent.toLowerCase().indexOf("icab")>-1)
auf iCab und if (navigator.userAgent.toLowerCase().indexOf("opera")>-1)
auf Opera. Strategie 1: Verschiedene Seiten für verschiedene Browser Nachdem wir unsere Besucher nun in hübsche Gruppen einteilen können, müssen wir uns überlegen, was wir dort mit ihnen anfangen. Zum Beispiel könnten wir mehrere Versionen der Seite erstellen und auf der Startseite die Browser entsprechend umleiten. Das sieht dann ungefähr so aus:
BrowserUmleitung
if (document.getElementById) // W3C-kompatibel location.replace("w3cdhtml.html"); else if (document.layers) // Netscape 4 location.replace("netscape4dhtml.html"); else if (document.all) // IE4 location.replace("ie4dhtml.html"); else location.href = "nodhtml.html";
Hier werden Netscape 4, Internet Explorer 4 und W3C-kompatible Browser (sowie solche, die vorgeben W3C-kompatibel zu sein) auf eine jeweils eigene Seitenversion geschickt. Alle anderen Browser kommen, sofern sie JavaScript
225
10 DHTML: Cross-Browser-Strategien verstehen, nach nodhtml.html. Die Puristen, die kein JavaScript verstehen, bleiben einfach auf der Umleitungsseite stehen, wo wir Ihnen beispielsweise einen Link nach nodhtml.html anbieten können. Verwenden Sie für JavaScript-Umleitungen möglichst immer location.replace() und nicht location.href. Denn nur bei location.replace kommt man mit dem Back-Button des Browsers von der DHTML-Seite aus wie gewünscht zurück auf die Seite, auf der man vorher war (vgl. Kapitel 3). Browser-Umleitungen haben den Vorteil, dass Sie auf den DHTML-Seiten keine Rücksicht mehr auf Inkompatibilitäten zu nehmen brauchen. Es gibt einfach für jedes Objektmodell eine eigene Seite. Der Nachteil ist, dass Sie auf diese Weise schnell sehr viele Seiten erhalten, die sich nur schwer warten lassen. Außerdem müssen Sie zum Beispiel dafür sorgen, dass, wenn jemand einen Link auf Sie setzt, dieser immer auf die Umleitungsseite führt. Strategie 2: Verschiedene Anweisungen für verschiedene Browser Häufig ist es bequemer, nur eine einzige Seite für alle Browser zu haben. In diesem Fall ist dafür zu sorgen, dass jede Browserversion nur diejenigen SkriptBereiche zu sehen bekommt, die sie versteht. Die Puristen bereiten hier wenig Schwierigkeiten: Sie ignorieren ohnehin alle Skripte. Schwieriger wird es mit den Browsern der zweiten Gruppe, die JavaScript kennen und deshalb versuchen werden, die DHTML-Funktionen auszuführen – um dabei unweigerlich zu scheitern. Dieses Problem ließe sich im Grunde einfach lösen, indem man in allen scriptTags das Attribut language auf JavaScript1.2 setzt. Ihr Inhalt wird dann von Netscape und Internet Explorer erst ab Version 4 ausgewertet. Leider geben jedoch auch die neueren Opera-Versionen vor, JavaScript1.2 zu unterstützen, und Netscape 3 ignoriert Sprachangaben bei extern eingebundenen Skripten völlig. SkriptVerzweigung
Am besten verzweigt man deshalb in den Skript-Blöcken selbst je nach Browser auf die entsprechenden Anweisungen: <script language="JavaScript1.2">
226
Grundlagen ... Anweisungen für alle anderen ... } // -->
Von diesen Verzweigungen braucht man in der Praxis eine ganze Menge, denn bei DHTML kommt fast in jedem Skriptblock, in jeder Funktion zum Beispiel, etwas vor, was nicht alle Browser verstehen. Es ist deshalb üblich, am Anfang der Seite Abkürzungen anzulegen: var n4 = document.layers; var ie = document.all; var w3c = document.getElementById;
Dann sieht von da an eine Abfrage auf Netscape 4 zum Beispiel nur noch so aus: if (n4) { ... }
Am besten fangen Sie an der Stelle auch die Browser ab, die das jeweilige Objektmodell nicht ausreichend verstehen. Wenn Sie wie ich finden, Opera habe in der w3c-Kategorie nichts verloren, dann sieht die Abfrage aufs W3C DOM so aus: var w3c = (document.getElementById && (navigator.userAgent.toLowerCase().indexOf("opera")==-1)
Strategie 3: Cross-Browser-Funktionen Nehmen wir an, Sie möchten die visibility-Eigenschaft eines positionierten div-Elements ändern. Cross-Browser geht das so: if (document.getElementById) // W3C document.getElementById("divID").style.visibility = "hidden"; else if (document.layers) // Netscape 4 document.layers["divID"].visibility = "hidden"; else if (document.all) // IE 4/5 document.all["divID"].style.visibility = "hidden";
So etwas ist in Ordnung, wenn es ein- oder zweimal auf einer Seite verwendet wird. Spätestens beim zwanzigsten Wechsel von visibility werden Sie diese Vorgehensweise aber als ziemlich unhandlich empfinden. Besser wäre es, wenn man eine Funktion hätte, die die ganze Verzweigung übernimmt, und die man jedes Mal nur noch aufzurufen braucht. Wir definieren also eine Cross-Browser-Funktion verstecke(), der die ID eines positionierten Elements übergeben wird, woraufhin sie dessen visibilityEigenschaft auf "hidden" schaltet:
227
10 DHTML: Cross-Browser-Strategien function verstecke(id) { if (document.getElementById) { // W3C document.getElementById(id).style.visibility = "hidden"; } else if (document.layers) { // Netscape 4 document.layers[id].visibility = "hidden"; } else if (document.all) { // IE 4/5 document.all[id].style.visibility = "hidden"; } }
Der Vorteil dürfte einleuchten: Ist die Funktion einmal definiert, brauchen Sie nur noch verstecke("elementID") aufzurufen, um das Element mit der ID elementID unsichtbar zu machen. Die inkompatiblen Objektmodelle der einzelnen Browser brauchen Sie dabei nicht mehr zu interessieren. Cross-BrowserLibraries
Wenn Sie DHTML auf mehr als nur einer Seite einsetzen, dann können Sie sich eine Reihe allgemein verwendbarer Cross-Browser-Funktionen basteln, diese in einem externen Skript-File ablegen und auf jeder DHTML-Seite einbinden. Der Cross-Browser-Code wird durch diese Funktionen wesentlich einfacher und übersichtlicher – Browser-Verzweigungen sind auf der Seite selbst fast gar nicht mehr nötig. Wir werden im Übungsteil einige Cross-Browser-Funktionen schreiben. Fertige und umfassendere Bibliotheken finden Sie im Internet, zum Beispiel auf http://www.brainjar.com/dhtml/dhtmllib.html. Strategie 4: Cross-Browser-Objekte JavaScript ist zwar keine streng objektorientierte Sprache wie Java, aber trotzdem lässt sich damit recht gut objektorientiert programmieren. Folglich kann man statt Cross-Browser-Funktionen auch eigens erstellte Cross-BrowserObjekte verwenden. Angenommen, Sie haben einen DHTML-Newsticker auf Ihrer Seite. Dann könnten Sie einen Objekttyp Ticker definieren und für Ihren Newsticker eine Instanz dieses Typs erzeugen: newsticker = new Ticker();
Zur Steuerung des Tickers verwenden Sie Methoden, die Sie für das Objekt definiert haben, um Beispiel: newsticker.stop();
oder newsticker.richtungswechsel();
228
Grundlagen Keine Angst, wenn Ihnen das im Moment nicht allzu viel sagt. In den Übungen zu diesem Teil wird genau erklärt, wie die einzelnen Schritte funktionieren und wozu das Ganze gut ist. Auch fertige Bibliotheken mit nützlichen Cross-Browser-Objekten gibt es im Internet, etwa unter http://www.dansteinman.com/dynduo/. (Mehr und aktuellere Links finden Sie auf der Webseite zu diesem Buch: http://www.javascriptworkshop.de/.) Noch ein paar Grundregeln Die wichtigste Regel für DHTML ist wahrscheinlich: Testen. Die vorhandenen Browser enthalten in Sachen DHTML derart viele Bugs, dass Sie darauf vertrauen können, bei jeder Seite mindestens einem von ihnen zu begegnen. Wenn Sie regelmäßig Ihre Skripte testen, dann können Sie ziemlich genau herausfinden, ab wann etwas nicht mehr funktioniert und die entsprechende Stelle verändern. Andernfalls werden Sie Stunden damit verbringen, in einem 1.000-Zeilen-Code diese eine Stelle zu suchen, an der dem Browser etwas nicht gefällt.
Testen
Testen Sie mit möglichst vielen Browsern, wenn es geht auch auf verschiedenen Plattformen. Sie können ohne Weiteres mehrere Netscape-Versionen nebeneinander installieren – laden Sie sich also gleich ein paar davon herunter. Vor allem einen aus der Reihe 4.01-4.05 sollten Sie besitzen, da diese noch kein JavaScript1.3 verstehen. Leider ist es zumindest unter Windows praktisch unmöglich, mehrere Versionen des Internet Explorers gleichzeitig zu betreiben. Bevor Sie sich an die Programmierung machen, überlegen Sie sich erst einmal, wie Sie das gewünschte Ergebnis auf allen Ziel-Browsern realisieren können. Oft ist es hilfreich, dabei zuerst an Netscape 4 zu denken, denn dessen DHTML-Fähigkeiten sind in vieler Hinsicht die schwächsten. Ein Ansatz für Netscape 4 lässt sich in der Regel ohne allzu große Probleme auf Internet Explorer übertragen – anders herum geht das häufig nicht.
Nachdenken
Wollen Sie beispielsweise eine Animation erzeugen, dann könnten Sie dazu mit Internet Explorer die style-Eigenschaften eines beliebiges Elements verändern. Für Netscape 4 brauchen Sie aber einen Layer, weshalb Sie am besten von Anfang an ein positioniertes div-Element verwenden. Wenn Sie einen Ansatz für Netscape 4 und Internet Explorer gefunden haben, dann dürfte Ihnen das W3C DOM kein Kopfzerbrechen mehr bereiten: In der Regel können Sie genauso vorgehen wie beim Internet Explorer – lediglich die syntaktischen Unterschiede müssen berücksichtigt werden. Zum Beispiel heißt es beim W3C nicht document.all["elementID"], sondern document. getElementById("elementID"). Die schon bei Netscape 3 vorhandenen Collections document.images, document.forms usw. existieren übrigens im W3C DOM nach wie vor.
229
10 DHTML: Cross-Browser-Strategien
10.2
Übungen
DHTML-Übungen mit mehr Interaktion zwischen Webseite und Besucher finden Sie im Kapitel »Erweitertes Event-Handling«. leicht
1. Übung
Hier ist ein Stück aus einem HTML-Dokument:
Nichts ist besser als nichts.
Wie spricht man das Element mit der ID sohn in Netscape 4 an, wie im Internet Explorer, und wie im W3C DOM? leicht
2. Übung
Wie könnte man für alle drei Objektmodelle den sohn aus der vorigen Übung über seine clip-Eigenschaft(en) nachträglich unsichtbar machen? leicht
3. Übung
Welche Anweisungen verschieben für alle DHTML-Browser das Element vater aus Übung 3 an die Koordinaten 10/200? Nachdem dieses Skript ausgeführt wurde, wo steht dann das Element sohn? leicht
4. Übung
Nehmen wir an, Sie wollen beim Klick auf einen Link den Inhalt einer Tabellenzelle wechseln. Wie lässt sich das mit Internet Explorer und Netscape 6 erreichen? mittel
5. Übung
Gibt es zur vorigen Übung auch eine Lösung für Netscape 4? leicht
6. Übung
Schreiben Sie eine Seite, auf der stets die aktuelle Zeit im Format »Stunden:Minuten:Sekunden« angezeigt wird. Tauschen Sie dazu den Inhalt eines positionierten Elements einmal pro Sekunde durch die neue Zeit aus.
230
Übungen leicht
7. Übung
Programmieren Sie eine Funktion beschreibe(), die cross-browser den Inhalt eines positionierten Elements überschreibt. Der neue Inhalt wird als String an die Funktion übergeben, ebenso die ID des positionierten Elements. leicht
8. Übung
Wir wollen noch ein paar weitere Cross-Browser-Funktionen zusammenstellen. Schreiben Sie als Nächstes eine Funktion bewege(), die das Element mit der übergebenen ID an die ebenfalls als Parameter übergebenen Koordinaten stellt. mittel
9. Übung
Die nächste Funktion heißt schiebe(). Sie stellt ebenfalls das gewünschte Element an die gewünschte Position, allerdings nicht ruckartig, sondern in einer gleitenden Bewegung. Die Zahl der pro Schritt zurückzulegenden Pixel, also die Geschwindigkeit, wird als zusätzlicher Parameter übergeben. Wenn Sie Angst vor Zahlen haben, dann machen Sie eine Version, die das Element nur in einer Richtung verschiebt, also entweder horizontal oder vertikal. mittel
10. Übung
Die Objektmodelle von Netscape 4, Internet Explorer und W3C unterscheiden sich besonders stark beim Schreiben und Lesen der clip-Werte positionierter Elemente. Netscape 4 stellt dafür eine Reihe getrennter Eigenschaften bereit: clip.top; clip.right; clip.bottom; clip.left; clip.height; clip.width;
Beim Internet Explorer und ebenso beim W3C gibt es hingegen nur eine einzige style-Eigenschaft clip, in deren Wert, einem String, die vier clip-Kanten versteckt sind. Zum Beispiel: "rect(0px 200px 100px 10px)"
In der Praxis erweist sich das Modell von Netscape 4 meist als bequemer: String-Manipulationen sind mühsam, und die Reihenfolge der Kantenwerte bei IE und W3C (oben, rechts, unten, links) kann sich auch keiner merken. Schreiben Sie deshalb vier Cross-Browser-Funktionen zum Clippen: clipTop(), clipRight(), clipBottom() und clipLeft(). Wenn als Parameter nur die Element-ID übergeben wird, sollen diese Funktionen den aktuellen Wert der
231
10 DHTML: Cross-Browser-Strategien Kante, für die sie zuständig sind, zurückgeben. Wird hingegen zusätzlich ein weiterer Parameter übergeben, ändern sie die Kante auf den übergebenen Wert. leicht
11. Übung
Jetzt wollen wir unsere kleine Funktionsbibliothek einmal praktisch anwenden. Schreiben Sie eine Seite, deren Inhalt wie ein Filmabspann von unten nach oben über den Bildschirm läuft. Benutzen Sie dazu die Funktionen schiebe() und bewege(). Letztere brauchen Sie, um den Inhalt am Anfang an die Unterseite des Fensters zu bringen. Wie Sie die Fensterhöhe herausbekommen, erfahren Sie in Anhang 2 zu diesem Kapitel. mittel
12. Übung
Verbessern Sie die Lösung der vorigen Übung, so dass die Laufschrift erst mit 100px Abstand vom unteren Fensterrand erscheint und bereits 100px vor dem oberen Fensterrand wieder verschwindet. Das ergibt nicht nur einen hübschen Rand, es verhindert auch, dass man den Inhalt bereits vorzeitig lesen kann, indem man nach unten scrollt. Verwenden Sie weiterhin nur ein einziges positioniertes Element. mittel
13. Übung
Wir verbessern noch einmal unser Lauftext-Skript: Der Inhalt soll nun nicht mehr automatisch durchlaufen. Stattdessen stellen Sie zwei Links auf die Seite, mit denen man den Text nach oben bzw. nach unten bewegen kann. leicht
14. Übung
In den nächsten Übungen sehen wir uns an, wie man selbst definierte Objekttypen für den Cross-Browser-Einsatz anlegt. Auf den ersten Blick mag Ihnen dieser Weg, besonders wenn Sie mit objektorientierter Programmierung noch nicht so vertraut sind, vielleicht abwegig erscheinen. Für komplexe Seiten ist er aber oft die sauberste und übersichtlichste Methode. Wir werden einen Objekttyp XElement definieren, der für alle DHTML-Browser ungefähr das darstellt, was Netscape 4 durch den eingebauten Objekttyp Layer anbietet. Objekttypen definiert man durch eine Konstruktorfunktion: function XElement() { }
Wir können nun beispielsweise mit var grauschwarzeEbene = new XElement()
ein neues Objekt (eine Instanz) vom Typ XElement erzeugen.
232
Übungen Eine leere Konstruktorfunktion macht in der Regel wenig Sinn. In der Konstruktorfunktion werden wir vielmehr diejenigen Eigenschaften definieren, die alle Instanzen des Objekttyps besitzen sollen. Das sieht dann zum Beispiel so aus: function XElement() { this.x = 0; this.y = 0; }
Die Eigenschaften x und y sollen für die Koordinaten des XElements (das heißt: seine linke obere Ecke) stehen. Wie in Kapitel 4 erklärt verweist this auf die jeweils durch den Aufruf des Konstruktors erzeugte Instanz. Jede Instanz des Objekttyps XElement soll für ein bestimmtes div-Element auf der HTML-Seite stehen. Diese Elemente liegen vermutlich nicht alle in der linken oberen Ecke des Browserfensters. Wir könnten dem Konstruktor die aktuelle Position in Form von zwei Funktionsargumenten übergeben: function XElement(x, y) { this.x = x; this.y = y; } var grauschwarzeEbene = new XElement(100, 200);
Wir werden aber anders vorgehen: Der Konstruktor bekommt nur eine einzige Argumentstelle, und zwar übergeben wir ihm die ID des div-Elements. Es ist dann Aufgabe der Konstruktorfunktion, herauszufinden, an welcher Position sich das Element mit dieser ID befindet. Schreiben Sie nun diesen Konstruktor. mittel
15. Übung
Angenommen, auf einer HTML-Seite befindet sich ein positioniertes div-Element:
ein Gedanke
Mit Hilfe des Konstruktors XElement() wird ein Objekt angelegt, das dieses Element repräsentiert: var grauschwarzeEbene = new XElement("grauschwarz");
Was passiert, wenn Sie anschließend folgende JavaScript-Anweisungen aufrufen?
233
10 DHTML: Cross-Browser-Strategien alert(grauschwarzEbene.y); grauschwarzeEbene.y = 50; alert(grauschwarzeEbene.y); leicht
16. Übung
Erweitern Sie die Konstruktorfunktion XElement(), so dass sie neben der Position des Elements auch dessen Breite und Höhe sowie seinen z-index-Wert als Eigenschaften widerspiegelt. Außerdem sollen XElement-Objekte eine Eigenschaft el besitzen, die auf das HTML-Element verweist, das sie repräsentieren. Wie Sie die Breite und Höhe eines Elements herausbekommen, steht in Anhang 2. mittel
17. Übung
Es empfielt sich, neben el – der Referenz auf das repräsentierte Element selbst – noch eine Eigenschaft bereitzustellen, die auf die style-Property des Elements verweist. Nennen wir diese Eigenschaft css. Netscape 4 kennt kein style-Objekt, deshalb zeigt css hier einfach auf das Element selbst. Der Konstruktor sieht jetzt so aus: function XElement(id) { if (document.getElementById) { this.el = document.getElementById(id); this.css = this.el.style; } else if (document.all) { this.el = document.all[id]; this.css = this.el.style; } else if (document.layers) { this.el = document.layers[id]; this.css = this.el; } this.x = parseInt(this.css.left); this.y = parseInt(this.css.top); this.zIndex = this.css.zIndex; if (document.layers) { this.breite = this.el.document.width; this.hoehe = this.el.document.height; } else { this.breite = this.el.offsetWidth; this.hoehe = this.el.offsetHeight; } }
234
Übungen Bisher ist unser Cross-Browser-Objekt noch ziemlich nutzlos. Das ändert sich, wenn wir ihm jetzt ein paar Methoden verpassen. Objekt-Methoden werden genau wie Objekt-Eigenschaften definiert. Der Unterschied ist nur, dass dem Methodennamen als Wert die Referenz auf eine Funktion zugewiesen wird (also der Funktionsname ohne Klammern, vgl. Kapitel 4). Mit Hilfe von Methoden lösen wir die Schwierigkeit, der wir in Übung 15 begegneten. Sie erinnern sich: Die Änderung von XElement-Eigenschaften wirken sich nicht auf das repräsentierte HTML-Element aus. Geben Sie dem Objekttyp XElement eine Methode gehNach(x,y), die das HTML-Element an die übergebenen Koordinaten stellt. Die x- und y-Eigenschaften des XElement-Objekts sollen natürlich dabei auch aktualisiert werden. leicht
18. Übung
Mittlerweile sollte Ihnen dämmern, wozu Cross-Browser-Objekttypen gut sein können: Immer wenn Sie ein positioniertes Element definiert haben, das Sie hin- und herbewegen wollen, brauchen Sie nur eine entsprechende Instanz von XElement zu erzeugen und können dann dessen gehNach()-Methode benutzen, ohne sich um die drei verschiedenen Objektmodelle Gedanken zu machen. Und sollte demnächst zur allgemeinen Freude noch ein viertes Objektmodell auftauchen, dann brauchen Sie nur den Konstruktor XElement() zu verändern. Die konkreten Anwendungen bleiben, solange sie nur die Objektmethoden einsetzen, unberührt. Im Gegensatz zu Cross-Browser-Funktionen sind außerdem zusammengehörige Variablen und Funktionen auch ordentlich zusammen abgelegt. Erweitern Sie XElement noch um zwei weitere Methoden: zeige() und verstecke(). mittel
19. Übung
Als erste Anwendung des Cross-Browser-Objekttyps XElement lassen wir es nun regnen. Schreiben Sie eine Webseite mit 50 kleinen, grauen, länglichen Layern: die Regentropfen. Jeder Regentropfen wird durch ein eigenes XElement repräsentiert. Regentropfen fallen, wie Sie wissen, gewöhnlich von oben nach unten. Wenn sie am unteren Fensterrand angekommen sind, stellen Sie sie wieder nach ganz oben. Die horizontale Position überlassen Sie am besten dem Zufall. schwer
20. Übung
Setzen Sie XElement() jetzt ein, um das nicht besonders originelle, aber vermutlich beliebteste DHTML-Skript zu erstellen: Ein Menü, das ähnlich wie die Dateimanager vieler Betriebssysteme beim Klick auf einen Hauptpunkt eine Reihe von Unterpunkten einblendet und bei erneutem Klick wieder ausblendet.
235
10 DHTML: Cross-Browser-Strategien Ihr Menü braucht nur eine Untermenütiefe zu unterstützen, das heißt, Sie können annehmen, dass keine Unterpunkte im Menü vorgesehen sind, die selbst wieder Unterunterpunkte einblenden. schwer
21. Übung
Entwickeln Sie einen neuen Objekttyp KlappMenu, der ein ganzes Menü von der Art, wie wir es in der letzten Übung gebaut haben, repräsentiert. Im Unterschied zu XElement() erzeugt der KlappMenu-Konstruktor selbst die Elemente, die er repräsentiert. Das bedeutet: Im Konstruktor wird mit document.write der HTML-Quelltext des Menüs geschrieben. Objekte vom Typ KlappMenu müssen deshalb erzeugt werden, noch bevor die Seite fertig gerendert ist. Dem Konstruktor werden neben der Position des Menüs zwei Arrays übergeben: eine Liste mit den Hauptmenü-Punkten und eine mit den Untermenüs. Die Elemente des zweiten Arrays sind also selbst wieder Arrays, sie enthalten die Namen der Untermenü-Punkte und den dazugehörenden URL. Ein Aufruf könnte also wie folgt aussehen: ... <script> klappHaupt = new Array("Früchte", "Tonarten"); klappSub = new Array( new Array("Apfel", "apfel.html", "Birne", "birne.html", "Kirsche", "kirsche.html"), new Array("a Moll", "amoll.html", "cis Dur", "cisdur.html") ); klapp = new KlappMenu(20, 100, klappHaupt, klappSub);
Diese Anweisungen sollen ausreichen, um das gewünschte Menü, voll funktionsfähig, an der gewünschten Position auf der Seite erscheinen zu lassen. mittel
22. Übung
Eines haben wir bislang sowohl bei den Cross-Browser-Funktionen als auch bei den Methoden von XElement übersehen: Layer können ineinander verschachtelt sein:
236
Übungen Hier liegt für Netscape 4 das eine positionierte div-Element im document des andern. Wenn wir die ID tochter an den XElement-Konstruktor oder eine der Cross-Browser-Funktionen übergeben, erhielten wir eine Fehlermeldung, denn document.layers["tochter"]
existiert nicht. Die korrekte Referenz wäre zum Beispiel: document.layers["mutter"].document.layers["tochter"].
Wie könnte man diese Schwierigkeit umgehen? leicht
23. Übung
Viele der in diesem Kapitel vorgestellten Lösungen führen auf DHTML-unkundigen Browsern zu Fehlermeldungen. Zum Abschluss deshalb noch zwei Aufgaben zum Umgang mit solchen Besuchern. Häufig legt man, besonders bei komplexen Auftritten, neben der DHTML-Seite eine Alternativseite für alle die Browser an, die kein DHTML beherrschen. Es handelt sich dabei meist um eine einfache HTML-Seite, die die wichtigsten Inhalte und Links des Angebots zusammenfasst. (Bei Angeboten ohne Inhalt enthält sie folgerichtig nur den Hinweis, dass man den falschen Browser verwendet.) Schreiben Sie eine Browser-Umleitung, die Netscape ab Version 4 und die Windows-Versionen von Internet Explorer 4 und 5 zur DHTML-Seite schickt, alle anderen dagegen auf die Alternativ-Seite. Überlegen Sie sich, ob man auch Browser, die kein JavaScript verstehen, direkt zur Alternativseite bringen könnte (und wenn ja, wie). mittel
24. Übung
Würden Sie für jede Browserversion eine eigene Seite schreiben, dann hätten Sie am Ende etwa 200 Versionen, die bei jeder Änderung alle überarbeitet werden müssen. Am schönsten wäre es deshalb, eine einzige Seite für alle zu haben. Die DHTML-Seite sollte also am besten auch mit Browsern betrachtet werden können, die kein DHTML oder sogar kein JavaScript verstehen. Ob das möglich ist, müssen Sie im Einzelfall selbst entscheiden. Bei der folgenden Seite ist es möglich. Allerdings muss dazu der Quelltext etwas umgebaut werden. Versuchen Sie es. <script> function menuEinAus(){ ... } function zeige(seite){ ...
237
10 DHTML: Cross-Browser-Strategien }
Bla bla
10.3
Tipps
Tipp zu 4: 왘 Geben Sie der Zelle eine ID.
Tipps zu 5: 왘 Wie kann man bei Netscape 4 Element-Inhalte wechseln? 왘 Nur bei Layer-Objekten lässt sich der Inhalt wechseln, und zwar mit der
document.write()-Methode. Wie lässt sich das für diese Aufgabe einsetzen? 왘 Theoretisch könnte man in die Zelle ein relativ positioniertes Element stel-
len, dessen Inhalt ausgetauscht wird. Das funktioniert aber nicht, weil der Navigator nur den Inhalt von absolut positionierten Layern überschreiben kann. (Es handelt sich um einen Bug, vgl. Netscape-Checkliste in Anhang 1.) Was man dagegen auch bei relativ positionierten Elementen ändern
238
Tipps kann, ist die visibility-Eigenschaft. Sie können also den Inhalt der Tabellenzelle, in ein relativ positioniertes span verpackt, sichtbar und unsichtbar machen. Hilft das weiter? 왘 Ja. Sie können nämlich auch die Koordinaten des relativ positionierten Lay-
ers ermitteln (siehe Anhang 2). 왘 An diese Koordinaten schieben Sie einen zweiten Layer, der absolut positio-
niert ist. Dessen Inhalt wechseln Sie dann mit document.write. Tipps zu 6: 왘 Wie Sie den Stunden-, Minuten- und Sekundenwert der aktuellen Zeit
herausbekommen, erfahren Sie in Kapitel 8. 왘 Übung 30 von Kapitel 9 zeigte Ihnen, dass beim Internet Explorer 5 Zeilen-
umbrüche und Leerzeichen im Quelltext nicht in TextNodes umgewandelt werden. (Davon ist übrigens nur die Windows-Version betroffen.) Ein Element wie
hat also keine ChildNodes. Erst wenn Sie beispielsweise hineinschreiben, bekommt uhr ein Kind, dessen nodeValue geändernt werden kann. Tipp zu 7: 왘 Sie brauchen nur die Schreib-Funktion aus der letzten Übung ein wenig zu
verallgemeinern. Tipps zu 9: 왘 Berechnen Sie, um wie viele Pixel das Element bei jedem Schritt nach rechts
und um wie viele es nach unten bewegt werden muss. Dabei helfen Ihnen die Funktion Math.sqrt(), die die Quadratwurzel einer Zahl liefert, und der Satz des Pythagoras. 왘 Rechnen Sie die horizontale und die vertikale Entfernung vom Zielpunkt
aus. Der Satz des Pythagoras liefert Ihnen dann die Gesamt-Entfernung. Was sagt Ihnen der Quotient aus horizontaler Entfernung und GesamtEnfernung? 왘 Er sagt Ihnen, um wie viel sich das Element in horizontaler Richtung
bewegt, wenn es einen Pixel auf das Ziel zugeht. Um wie viele Pixel muss also das Element pro Schritt nach rechts geschoben werden? 왘 Die Zahl der Pixel ist: (HorizontaleDistanz/GesamtDistanz)*Geschwindig-
keit.
239
10 DHTML: Cross-Browser-Strategien 왘 Wenn die Gesamt-Entfernung kleiner als die übergebene Geschwindigkeit
ist, stellen Sie das Element an den Zielpunkt und beenden die Bewegung. Vergessen Sie nicht, dass eine Bewegung auch nach links oder oben gehen kann! Tipps zu 10: 왘 Selbst wenn Sie nur einen clip-Wert setzen wollen, müssen Sie für Inter-
net Explorer und W3C alle vier Kanten-Abstände angeben. Dazu brauchen Sie die aktuellen Werte der anderen Kanten. 왘 Am einfachsten bekommen Sie diese Werte durch einen Aufruf der drei
jeweils anderen clip-Funktionen. 왘 Versuchen Sie, aus dem style.clip-String die vier Werte herauszulesen. 왘 Das geht zum Beispiel so:
var clipWerte = style.clip.split("rect(")[1].split(")")[0].split("px"); Diese etwas kryptische Anweisung liefert bei W3C und IE einen Array mit den vier Werten. Tipps zu 12: 왘 Sie brauchen die Funktionen clipTop() und clipBottom(). Vergessen Sie
nicht, dem div-Element clip-Werte zuzuweisen, bevor Sie diese Funktionen benutzen, sonst können Internet Explorer und Netscape 6 die aktuellen Werte nicht ermitteln. 왘 Die clip-Eigenschaften müssen bei jedem Bewegungsschritt angepasst wer-
den. Deshalb können Sie schiebe() nicht mehr verwenden. Sie müssen mit bewege(), clipTop() und clipBottom() auskommen. 왘 Sowohl Netscape als auch Internet Explorer zeigen gerne den vertikalen
Scrollbalken, obwohl er eigentlich gar nicht gebraucht wird. In so einem Fall setzen Sie bei Netscape 4 document.height auf den Wert von win dow.innerHeight, bei W3C-kompatiblen Browsern geben Sie dem bodyElement das CSS-Attribut overflow:hidden und setzen document.body. height auf die Fenster-Innenhöhe. Tipps zu 13: 왘 Schreiben Sie eine Funktion hoch(), die den Inhalt nach unten bewegt, und
eine Funktion runter(), die ihn nach oben schiebt. (Irritiert? Dann sehen Sie sich eine gewöhnliche Scrollleiste an: Wenn Sie auf den Abwärts-Pfeil drücken, bewegt sich der Fensterinhalt nicht etwa nach unten, sondern nach oben.)
240
Tipps 왘 Rufen Sie diese Funktionen im entsprechenden Link bei onmousedown auf.
onmouseup muss die Bewegung angehalten werden. Wie könnte man das erreichen? 왘 Definieren Sie eine globale Variable laeuft, die auf true gesetzt ist, wenn
der Inhalt gerade scrollt, andernfalls auf false. In den Funktionen hoch() und runter() sehen Sie dann nach, welchen Wert laeuft gerade hat und brechen im Fall von false die Bewegung ab. Beim ersten Aufruf der Funktionen müssen Sie laeuft auf true setzen. Die onmouseup-Anweisung lautet dann einfach: laeuft=false. Tipp zu 14: 왘 Vergessen Sie nicht, in der Konstruktorfunktion die drei gängigen Objekt-
modelle zu berücksichtigen. Bei Netscape 4 bekommen Sie die Position nicht auf dieselbe Weise wie bei Internet Explorer 4 oder beim W3C. Tipps zu 19: 왘 Wie Sie die Fensterhöhe und -breite herausbekommen, steht in Anhang 2. 왘 Erzeugen Sie die Tropfen-Elemente mit document.write() in einer Schleife.
Auch für die XElemente brauchen Sie keine 50 Variablen, nehmen Sie stattdessen einen Array. 왘 Math.random() gibt Ihnen eine Zufallszahl zwischen 0 und 1. 왘 Die Style-Definition für die Regentropfen könnte etwa so aussehen:
.tropf { position:absolute; width:2px; height:8px; clip:rect(0px 2px 8px 0px); background-color:#999999; layer-backgroundcolor:#999999 }
Tipps zu 20: Es gibt eine beachtliche Anzahl solcher Menüs im Internet und fast ebenso viele Variationen in der technischen Umsetzung. Ich kann Ihnen deshalb nur eine von vielen Möglichkeiten hier vorstellen. Wenn Sie eine ganz andere Lösung gefunden haben, dann kann die genauso gut oder besser sein. Wenn Sie aber bei Ihrem eigenen Ansatz nicht mehr weiterkommen (was bei dieser Übung kein Grund zur Sorge ist) oder bereits mehrere hundert Programmzeilen verbraucht haben, dann versuchen Sie es mit den folgenden Tipps. 왘 Erstellen Sie zuerst die Menü-Einträge ganz normal in HTML. Sie müssen sie
nur so in positionierte Elemente verpacken, dass Sie später mit JavaScript darauf zugreifen können.
241
10 DHTML: Cross-Browser-Strategien 왘 Der Quelltext für einen Menü-Punkt und sein Untermenü könnte etwa so
aussehen:
Ich nehme also für jeden Hauptmenüpunkt ein eigenes positioniertes Element. Die Unterpunkte kommen dagegen jeweils zusammen in einen divContainer. Das Menü ist so angelegt, dass es auch ohne JavaScript bedienbar ist: Die Unterpunkte verweisen über ihr href-Attribut direkt auf die entsprechende Seite. Die JavaScript-Funktion zeige(), die die Seite anzeigt (in einem Layer, einem neuen Fenster oder wie immer Sie wollen), wird onclick aufgerufen. Ich spare mir die zweite Nennung des Dateinamens, indem ich im onclickHandler stattdessen this.href einsetze. this ist eine Referenz auf das Element, in dem der Event-Handler steht, in diesem Fall also auf den Link. this.href zeigt folglich auf den href-Wert des Links. return false blockiert das Anspringen des href-Werts, wie es normalerweise passieren würde, wenn man den einen Link klickt. Noch dazu hat es den angenehmen Nebeneffekt, im Gegensatz zu
...
den Mauszeiger nicht in eine Sanduhr zu verwandeln und animierte gif-Grafiken auf der Seite weiter laufen zu lassen. Die CSS-Klassen .haupt und .sub enthalten lediglich Angaben über das Aussehen der Links. Zum Beispiel wollen wir nicht, dass die Menü-Einträge unterstrichen sind, und vielleicht sollen die Hauptmenü-Punkte etwas größer geschrieben sein als die Unterpunkte. 왘 Im head-Bereich der Seite bauen Sie jetzt den Konstruktor XElement() ein.
Eine Initialisierungsfunktion soll onload für jedes Menü-div ein XElementObjekt anlegen. Versuchen Sie es. Lesen Sie im nächsten Tipp, wie ich es gemacht habe.
242
Tipps 왘 Es erleichtert die Aufgabe, wenn Sie wie bei den Regentropfen eine
Namenskonvention für die Element-IDs einhalten. Bei mir heißen die Haupt-Einträge menu1, menu2 usw., die dazugehörigen Untermenüs menu1sub, menu2sub usw. Damit kann man zur Konstruktion der XElementObjekte eine for-Schleife benutzen. Wir müssen nur wissen, wie oft die Schleife durchlaufen werden soll. Im Grunde wäre es möglich, dies automatisch zu ermitteln, indem wir bei jedem Durchgang überprüfen, ob ein Element namens menuN existiert, wobei N für den aktuellen Wert der Zählvariablen steht. Ich mache es mir aber leicht und setze stattdessen eine globale Variable hauptMenuPunkte auf den entsprechenden Wert. Wenn man die Zahl der Menüpunkte ändert, muss eben jedesmal der Wert dieser Variablen angepasst werden. Bei der Gelegenheit definiere ich auch gleich zwei weitere globale Variablen, menuX und menuY, die die gewünschte Position (links/oben) des Menüs enthalten. hauptMenuPunkte = 3; menuY = 100; menuX = 20; function init() { hauptMenu = new Array(); subMenu = new Array(); for (i=1;i<=hauptMenuPunkte;i++) { hauptMenu[i] = new XElement("menu"+i); subMenu[i] = new XElement("menu"+i+"sub"); subMenu[i].verstecke(); } ordnen(); }
Wie Sie sehen, lege ich zwei Arrays an: Eines enthält die HauptmenüPunkte, das andere die dazugehörigen Untermenüs. Da letztere am Anfang alle ausgeblendet sind, verstecke ich sie auch sofort mit der entsprechenden XElement-Methode. Am Ende der init()-Funktion wird die Funktion ordnen() aufgerufen. Und das ist die nächste Aufgabe: 왘 Wir wollen die Menü-Einträge untereinander anordnen. Bei jedem Auf-
und Zuklappen eines Untermenüs muss diese Ordnung natürlich revidiert werden, denn beim Aufklappen eines Menüpunkts verschiebt sich die Position aller darunter liegenden Punkte nach unten, umgekehrt beim Zuklappen. Die Funktion ordnen() wird deshalb bei jeder Menü-Änderung aufgerufen. Sie sieht nach, welche Menüpunkte aufgeklappt sind, und positioniert die nachfolgenden Punkte entsprechend. Der nächste Tipp verrät Ihnen, wie die Funktion bei mir aussieht.
243
10 DHTML: Cross-Browser-Strategien 왘 Ich gehe einfach das Menü von oben nach unten durch. Eine Variable posY
zählt die vertikale Position mit, an der ich gerade stehe. Am Anfang steht sie auf menuY, dem Wert für die Oberkante des Menüs. Für jedes sichtbare Element addiere ich die Element-Höhe: function ordnen() { var posY = menuY; for (i=1;i<=hauptMenuPunkte;i++) { hauptMenu[i].gehNach(menuX,posY); posY+=hauptMenu[i].hoehe; if (subMenu[i].sichtbar) { subMenu[i].gehNach(menuX+20,posY); posY+=subMenu[i].hoehe; } } } 왘 Jetzt sind wir fast fertig. Es fehlt nur noch die Funktion klapp(). Sie schaut
nach, ob das übergebene Untermenü sichtbar ist, schaltet es entsprechend an oder aus und ruft dann ordnen() auf. Die ganze Lösung am Stück finden Sie im Lösungsteil. Tipps zu 21: 왘 Definieren Sie alle Funktionen und Variablen aus der letzten Übung als
Methoden und Eigenschaften von KlappMenu. Aus der Funktion klapp() wird zum Beispiel die Methode: this.klapp = KlappMenu_klapp; ... function KlappMenu_klapp(nr) { if (this.subMenu[nr].sichtbar) this.subMenu[nr].verstecke(); else this.subMenu[nr].zeige(); this.ordnen(); } 왘 Der schwierigste Teil der Übung besteht darin, das Menü korrekt mit docu-
ment.write ins Dokument zu schreiben. Hier müssen Sie die übergebenen Arrays durchgehen und um die Array-Einträge herum die div- und Link-Elemente aufbauen. Es gibt dabei zwei besondere Schwierigkeiten: Zum einen muss jede div-ID eindeutig sein. Auch wenn die Konstruktorfunktion mehrmals aufgerufen wird, darf kein Name doppelt vergeben sein. Es empfielt sich deshalb, eine globale Variable klappMenuZaehler anzulegen, die bei jedem Konstruktor-Aufruf um eins hochgezählt wird. In die IDs der dynamisch erzeugten Elemente bauen Sie dann die Nummer der aktuellen Instanz mit ein.
244
Tipps Ein weiteres Problem ist, dass Sie in den Links Methoden der jeweiligen KlappMenu-Instanz aufrufen wollen. So funktioniert es zum Beispiel nicht: ' ... onclick="'+this+'.zeige(this.href) ... '
Hier wird der Broswer this über dessen toString()-Methode in einen String verwandeln. Je nach Browser kommt dabei im dynamisch geschriebenen Quelltext etwas heraus wie: ... onclick="[object Object].zeige(this.href) ...
Und das führt natürlich bei einem Klick nur zu einer Fehlermeldung. Sie brauchen eine globale Variable, die auf die aktuelle Objektinstanz verweist. Eine solche wird bei der Instanz-Erzeugung angelegt: klapp = new KlappMenu(...)
Leider können Sie aber in der Konstruktorfunktion nicht herausbekommen, wie diese Variable (hier klapp) heißt. Ein möglicher Ausweg ist, dem Konstruktor als zusätzliches Argument den Variablennamen zu übergeben. Sie können aber ebenso gut eine neue globale Variable vom Konstruktor aus anlegen: function KlappMenu(...) { dieseVariableVerweistAufDieErzeugteInstanz = this;
Auch hier brauchen Sie jedoch einen eindeutigen Namen, der bei jedem Konstruktoraufruf wechselt. Setzen Sie also wieder den Zähler ein. Beispielsweise könnten Sie die Variable so definieren: function KlappMenu(...) { self["klappMenu"+klappMenuZaehler] = this;
Ist der klappMenuZaehler zum Beispiel gerade bei 4, entsteht so die globale Variable klappMenu4, die auf die gerade erzeugte KlappMenu-Instanz zeigt. Falls die Schreibweise mit den eckigen Klammern Sie irritiert: Globale Variablen sind in JavaScript Eigenschaften des window-Objekts. Die Anweisung bla = 1 ist nur eine Kurzform von window.bla = 1 oder self.bla = 1. Wie jede Objekt-Eigenschaft können Sie auch die Eigenschaften von window nicht nur über den Punkt-Operator ansprechen, sondern auch über eckige Klammern, in denen der Name der Eigenschaft als String angegeben wird. self["klappMenu"+4] = this ist also gleichbedeutend mit self.klapp Menu4 = this oder kurz klappMenu4 = this. Die erste Version erlaubt jedoch, bei der Definition Variablen einzusetzen und String-Operationen (wie »+«) zu benutzen, ohne dabei eval zu bemühen. 왘 Netscape 4 mag es nicht, wenn div-Tags mit style-Attribut per document.
write() geschrieben werden. Definieren Sie deshalb eine CSS-Klasse .abs { position:absolute }
und schreiben Sie in die div-Tags statt der inline-style-Angabe: class="abs".
245
10 DHTML: Cross-Browser-Strategien Tipp zu 22: 왘 Es gibt mal wieder mehrere Ansätze. Einer wäre, der Funktion nicht die ID,
sondern gleich eine Referenz auf das Element zu übergeben. Dann müsste man allerdings beim Aufruf immer eine Browser-Verzweigung durchführen. Häufig wird deshalb ein anderer Weg gewählt: Man übergibt der Konstruktorfunktion bei verschachtelten Layern neben der Element-ID noch ein zweites Argument, das den übergeordneten Layer angibt. Was muss in der Funktion geändert werden? Tipps zu 24: 왘 Sie müssen den (nicht abgedruckten) Inhalt der beiden DHTML-Funktionen
vor veralteten Browsern verstecken. Wenn Sie die Funktionen aber einfach in einen JavaScript1.2-Bereich stellen, liefern die alten Browser eine Fehlermeldung, weil beim Klick auf die Menübereiche für sie undefinierte Funktionen aufgerufen werden. Wie lässt sich das verhindern? 왘 Ändern Sie die Reihenfolge der Seitenelemente. Eins der Elemente ist für
DHTML-unfähige Browser überflüssig. Versuchen Sie es für diese unsichtbar zu machen.
10.4
Lösungen
Lösung zu 1: Mit Netscape 4: document.layers["vater"].document.layers["sohn"]
Mit Internet Explorer: document.all["sohn"]
Im W3C DOM: document.getElementById("sohn")
Lösung zu 2: Ein Element wird unsichtbar, wenn man beispielsweise die untere clip-Kante an die Element-Obergrenze legt, oder die rechte clip-Kante an die linke Element-Grenze, oder beides: if (document.getElementById) // W3C document.getElementById("sohn").style.clip = 'rect(0px 0px 0px 0px)'; else if (document.all) // IE document.all.sohn.style.clip = 'rect(0px 0px 0px 0px)'; else if (document.layers) // Netscape 4 document.vater.document.sohn.clip.bottom = 0;
246
Lösungen Lösung zu 3: if (document.getElementById) // W3C document.getElementById("vater").style.top = "200px"; else if (document.all) // IE document.all["vater"].style.top = "200px"; else if (document.layers) // Netscape 4 document.layers["vater"].top = 200;
Absolute Positionierung ist immer relativ zum nächsthöheren positionierten Element. Beim vater ist dies die Wurzel des Hauptdokuments, welche immer die Koordinaten 0/0 hat, beim sohn ist es das vater-Element. Mit dem vater wurde also auch der sohn verschoben: Er befindet sich jetzt an den Koordinaten 10/210. Es gibt dabei keine Unterschiede zwischen den Browsern. Lösung zu 4: Angenommen, die Tabellenzelle, die überschrieben werden soll, hat die ID »zelle«. Dann geht das beispielsweise so: function aendern(text) { if (document.getElementById) { var zelleEl = document.getElementById("zelle"); zelleEl.firstChild.nodeValue = text; } else if (document.all) { document.all.zelle.innerText = text; } }
Lösung zu 5: Und so sieht die Cross-Browser-Lösung aus: <script> function aendern(text){ if (document.getElementById) { var zelleEl = document.getElementById("zelle"); zelleEl.firstChild.nodeValue = text; } else if (document.all) { document.all.zelle.innerText = text; } else if (document.layers) { document.hilfsLayer.top = document.zelle.pageY; document.hilfsLayer.left = document.zelle.pageX; document.hilfsLayer.document.write(text); document.hilfsLayer.document.close(); document.zelle.visibility = "hidden";
247
10 DHTML: Cross-Browser-Strategien document.hilfsLayer.visibility = "visible"; } }
erste Zelle | <span id="zelle" style="position:relative"> zweite Zelle |
Zelleninhalt ändern
In Kapitel 9 habe ich erwähnt, dass auch td-Elemente durch Positionierung in Layer verwandelt werden. Anstatt in die Tabellenzelle ein span-Element zu setzen, ginge deshalb theoretisch auch:
zweite Zelle |
Die Erfahrung zeigt aber, dass positionierte Tabellenzellen häufig Ärger machen. Internet Explorer 4 kommt damit zum Beispiel überhaupt nicht klar. Ganz ähnlich können Sie übrigens vorgehen, um einen Rollover-Effekt bei TextLinks mit Netscape 4 zu erreichen. Zugegeben, für diese Lösung musste man um ein paar Ecken denken. Das Ergebnis ist immer noch nicht perfekt: Die Zellengröße passt sich beim Netscape Navigator nicht dynamisch an den Inhalt an – wenn man zu viel hineinschreibt, ragt der Inhalt einfach über die Zelle hinaus.
248
Lösungen Lösung zu 6: Die Zeit es ist:
<script language="JavaScript1.2">
249
10 DHTML: Cross-Browser-Strategien Lösung zu 7: function beschreibe(id, inhalt){ if (document.getElementById) { var elem = document.getElementById(id); elem.firstChild.nodeValue = inhalt; } else if (document.all) { document.all[id].innerHTML = inhalt; } else if (document.layers) { document.layers[id].document.write(inhalt); document.layers[id].document.close(); } }
Die Funktion arbeitet wie gewünscht, wenn das Ziel-Element nur einen einzigen ChildNode, nämlich einen TextNode, besitzt: Der Code fürs W3C-DOM überschreibt ja nur den nodeValue des ersten Kinds. Eventuelle andere Kinder bleiben unangetastet. Ferner können keine HTML-Tags in das Zielelement geschrieben werden. Hier zeigt sich, dass für viele Anwendungen die im DOM fehlende innerHTMLEigenschaft durchaus praktisch ist. Netscape hat deshalb beschlossen, in Version 6 innerHTML zu unterstützen. Wenn Sie sicher gehen, dass keine DOM-Puristen auf ihre Seite kommen, können Sie deshalb die Funktion folgendermaßen ändern: function beschreibe(id, inhalt){ if (document.getElementById) { document.getElementById(id).innerHTML = inhalt; } else if (document.all) { document.all[id].innerHTML = inhalt; } else if (document.layers) { document.layers[id].document.write(inhalt); document.layers[id].document.close(); } }
Lösung zu 8: function bewege(id, x, y) { if (document.getElementById) { document.getElementById(id).style.left = x+"px"; document.getElementById(id).style.top = y+"px"; } else if (document.all) {
250
Lösungen document.all[id].style.left = x+"px"; document.all[id].style.top = y+"px"; } else if (document.layers) { document.layers[id].left = x; document.layers[id].top = y; } }
Lösung zu 9: function schiebe(id, x, y, speed) { // Wir legen erst einmal eine Variable an, die bei allen // Browsern auf das Element verweist: if (document.getElementById) var elem = document.getElementById(id).style; else if (document.all) var elem = document.all[id].style; else if (document.layers) var elem = document.layers[id]; // Jetzt messen wir die aktuelle Position und berechnen // die horizontale und vertikale Entfernung vom Ziel: var jetztX = parseInt(elem.left); var jetztY = parseInt(elem.top); var distX = x – jetztX; var distY = y – jetztY; // Der Satz des Pythagoras gibt uns die // Gesamt-Entfernung: var dist = Math.sqrt(distX*distX + distY*distY); // Wenn der Gesamt-Abstand kleiner als speed ist, // Element ans Ziel stellen und abbrechen: if (Math.abs(dist) < speed){ elem.left = x; elem.top = y; return; } // Ansonsten das Element weiter schieben und diese // Funktion zeitverzögert wieder aufrufen: elem.left = jetztX + (distX/dist)*speed; elem.top = jetztY + (distY/dist)*speed; setTimeout('schiebe("'+id+'",'+x+','+y+','+speed+')',40); }
Vielleicht haben Sie es gemerkt: Die letzten Zeilen sind nicht ganz korrekt. Eigentlich sollte die Positionsangabe bei W3C-Browsern ein String mit Maßein-
251
10 DHTML: Cross-Browser-Strategien heit sein, also zum Beispiel »150px«. Internet Explorer 5 und Netscape 6 akzeptieren aber (wenn man keine strikte DTD einstellt) auch bloße Zahlen, deshalb habe ich mir an der Stelle die Browserverzweigung gespart. Lösung zu 10: Die Funktionen sind alle sehr ähnlich. Hier ist deshalb nur eine davon abgedruckt, Sie finden die anderen natürlich auf der Begleit-CD. function clipTop(id, wert) { // kein Problem mit Netscape 4: if (document.layers) { if (wert) document.layers[id].clip.top = wert; return document.layers[id].clip.top; } // Etwas schwieriger wird es mit IE und W3C: if (document.all) var stil = document.all[id].style; else var stil = document.getElementById(id).style; if (wert) stil.clip = "rect("+wert+"px " + clipRight(id)+"px " + clipBottom(id)+"px " + clipLeft(id)+"px)"; var clipWerte = stil.clip.split("rect(")[1].split(")")[0].split("px"); return clipWerte[0]; }
Wenn Sie mit Clipping arbeiten, sollten Sie die MacIntosh-Version von Internet Explorer 4 auf eine Alternativ-Seite umleiten. Sie unterstützt Clipping leider überhaupt nicht. Version 5 verhält sich in der Hinsicht besser (solange Sie nicht auto als clip-Wert benutzen, was ziemliche Verwirrung auslöst). Lösung zu 11: Um den Seiteninhalt scrollen zu lassen, stellen wir ihn in ein positioniertes divElement:
... hier steht der ganze Inhalt ...
Vorausgesetzt, die Funktionen bewege() und schiebe() sind auf der Seite eingebunden, brauchen wir nur noch folgende Zeilen aufzurufen, wenn die Seite fertig geladen ist: if (window.innerHeight) var fensterHoehe = window.innerHeight;
252
Lösungen else var fensterHoehe = document.body.clientHeight; bewege("inhalt", 0, fensterHoehe); schiebe("inhalt", 0, -1000, 2);
So wird der Inhalt einfach bis -1000 Pixel nach oben gezogen. Besser wäre natürlich, die Größe des Elements zu messen und den Wert entsprechend anzupassen. Lösung zu 12: function init(){ if (document.all) { fensterHoehe = document.body.clientHeight; fensterBreite = document.body.clientWidth; document.all["inhalt"].style.clip = "rect(0px "+fensterBreite+"px 0px 0px)"; } else { fensterHoehe = window.innerHeight; fensterBreite = window.innerWidth; if (document.layers) document.height = fensterHoehe; else if (document.getElementById) document.getElementById("inhalt").style.clip = "rect(0px "+fensterBreite+"px 0px 0px)"; } if (document.getElementById) document.body.style.height = fensterHoehe+"px"; posY = fensterHoehe-100; lauf(); } window.onload = init; function lauf(){ posY-=2; clipTop("inhalt", 100-posY); clipBottom("inhalt", fensterHoehe-100-posY); bewege("inhalt", 0, posY); if (posY>-1000) setTimeout("lauf()",40); }
Lösung zu 13: Hier erst einmal die beiden Links:
hoch
253
10 DHTML: Cross-Browser-Strategien
runter
Gewöhnen Sie sich an, bei MouseDown-Handlern immer ein return false einzufügen, weil andernfalls auf MacIntosh plötzlich das Kontextmenü angezeigt wird. Dann brauchen wir wieder die bekannte Initialisierung, die onload oder am Ende des body-Bereichs ausgeführt wird: function init(){ if (document.all) { fensterHoehe = document.body.clientHeight; fensterBreite = document.body.clientWidth; document.all["inhalt"].style.clip = "rect(0px "+fensterBreite+"px 0px 0px)"; } else { fensterHoehe = window.innerHeight; fensterBreite = window.innerWidth; if (document.layers) document.height = fensterHoehe; else if (document.getElementById) document.getElementById("inhalt").style.clip = "rect(0px "+fensterBreite+"px 0px 0px)"; } if (document.getElementById) document.body.style.height = fensterHoehe+"px";
Weil der Inhalt diesmal nicht gleich zu scrollen beginnt, müssen wir ihn bei der Initialisierung schon richtig positionieren und clippen: bewege("inhalt", 0, 50); clipTop("inhalt", 0); clipBottom("inhalt", fensterHoehe-100); }
Schließlich werden die beiden globalen Variablen und die Scroll-Funktionen definiert: posY = 50; laeuft = 0; function hoch(){ posY+=2; clipTop("inhalt", 50-posY); clipBottom("inhalt", fensterHoehe-50-posY); bewege("inhalt", 0, posY); if (laeuft) setTimeout("hoch()",40); }
254
Lösungen function runter(){ posY-=2; clipTop("inhalt", 50-posY); clipBottom("inhalt", fensterHoehe-50-posY); bewege("inhalt", 0, posY); if (laeuft) setTimeout("runter()", 40); }
Sie haben damit eine Seite, die man ohne die Standard-Scrollleiste des Browsers hoch- und runterscrollen kann. Um das Ganze schöner zu machen, können Sie anstelle der einfachen TextLinks hübsche Pfeil-Buttons nehmen, die Sie an der Ober- und Unterseite des Fensters positionieren. Sogar die normalerweise zwischen den Pfeilbuttons liegende Leiste mit dem beim Scrollen wandernden Balken lässt sich mit DHTML realisieren – allerdings nicht ohne einigen Aufwand. Lösung zu 14: function XElement(id) { if (document.getElementById) { var el = document.getElementById(id); this.x = parseInt(el.style.left); this.y = parseInt(el.style.top); } else if (document.all) { this.x = document.all[id].style.pixelLeft; this.y = document.all[id].style.pixelTop; } else if (document.layers) { this.x = document.layers[id].left; this.y = document.layers[id].top; } }
Lösung zu 15: Der erste Aufruf von alert(grauschwarzEbene.y)
zeigt die aktuelle y-Position des div-Blocks, also 200. Dann wird der y-Wert von grauschwarzeEbene mit grauschwarzeEbene.y = 50;
auf 50 gesetzt. Der zweite Aufruf von alert(grauschwarzEbene.y)
liefert deshalb 50.
255
10 DHTML: Cross-Browser-Strategien Das bedeutet aber nicht, dass der div-Block sich um 150 Pixel nach oben bewegt hat. Er steht immer noch an seiner alten Stelle. Es wurde der y-Wert des Objekts grauschwarzeEbene geändert, nicht der des HTML-Elements. Der JavaScript-Interpreter kann bei der Ausführung nicht ahnen, dass das Objekt grauschwarzeEbene dieses div-Element repräsentieren soll. Es gibt auch in JavaScript keine Möglichkeit, den y-Wert des Objekts mit dem style.top-Wert des HTML-Elements so zu koppeln, dass eine Änderung des einen immer eine Änderung des anderen nach sich zieht. Wir werden bald sehen, wie man diese Schwierigkeit umgeht. Merken Sie sich an dieser Stelle nur, dass ein Objekt vom Typ XElement für JavaScript nicht ein und dasselbe Objekt ist wie das div-Element, welches es repräsentiert. Lösung zu 16: Wir definieren zuerst die Eigenschaft el. Wir können sie dann gleich gebrauchen, um die anderen Werte zu bestimmen. function XElement(id) { if (document.getElementById) this.el = document.getElementById(id); else if (document.all) this.el = document.all[id]; else if (document.layers) this.el = document.layers[id]; if (document.layers) { this.x = this.el.left; this.y = this.el.top; this.zIndex = this.el.zIndex; this.breite = this.el.document.width; this.hoehe = this.el.document.height; } else { this.x = parseInt(this.el.style.left); this.y = parseInt(this.el.style.top); this.zIndex = this.el.style.zIndex; this.breite = this.el.offsetWidth; this.hoehe = this.el.offsetHeight; } }
Wie Sie sehen, erspart uns this.el die Verzweigung zwischen Microsoft- und W3C-Syntax: Bei beiden findet sich beispielsweise der z-index-Wert unter this.el.style.zIndex. Lösung zu 17: function XElement_gehNach(x,y) { // Element verschieben: this.css.left = x; this.css.top = y;
256
Lösungen // Werte aktualisieren: this.x = x; this.y = y; } function XElement(id) { ... die bisherigen Angaben ... this.gehNach = XElement_gehNach; }
Die css-Eigenschaft stellt sich hier als sehr nützlich heraus: Wir brauchen überhaupt keine Browser-Verzweigung. Vielleicht fragen Sie sich, wieso das jetzt funktioniert, wohingegen die einfache Änderung der x- und y-Eigenschaften keine Wirkung zeigte. Der Grund ist: Die Werte von XElement.x und XElement.y sind Kopien der entsprechenden div-Koordinaten. Ändern wir diese Kopien, dann bleiben die Originale unberührt. Dagegen sind XElement.el und XElement.css keine Kopien der HTML-Objekte bzw. ihrer Style-Eigenschaften, sondern Referenzen auf diese. Wenn wir deshalb css.left ändern, dann ändern wir tatsächlich die StyleEigenschaft left des HTML-Elements. In JavaScript werden grundsätzlich einfache Datentypen wie Zahlen und Zeichenketten bei einer Zuweisung kopiert, wohingegen bei komplexen Datentypen wie Objekten und Funktionen eine Referenz angelegt wird. In ihrem derzeitigen Umfang bietet JavaScript keine Einflussmöglichkeit auf dieses Verhalten. Lösung zu 18: Hier ist der vollständige Konstruktor, wie wir ihn in den nächsten Übungen verwenden wollen: function XElement(id) { if (document.getElementById) { this.el = document.getElementById(id); this.css = this.el.style; } else if (document.all) { this.el = document.all[id]; this.css = this.el.style; } else if (document.layers) { this.el = document.layers[id]; this.css = this.el; }
257
10 DHTML: Cross-Browser-Strategien this.x = parseInt(this.css.left); this.y = parseInt(this.css.top); this.zIndex = this.css.zIndex; this.sichtbar = true; if (document.layers) { this.breite = this.el.document.width; this.hoehe = this.el.document.height; } else { this.breite = this.el.offsetWidth; this.hoehe = this.el.offsetHeight; } this.gehNach = XElement_gehNach; this.zeige = XElement_zeige; this.verstecke = XElement_verstecke; } function XElement_gehNach(x,y) { this.css.left = x; this.css.top = y; this.x = x; this.y = y; } function XElement_zeige() { this.css.visibility = "visible"; this.sichtbar = true; } function XElement_verstecke() { this.css.visibility = "hidden"; this.sichtbar = false; }
Ein universell einsetzbarer Objekttyp müsste natürlich noch mehr können, zum Beispiel bräuchte er Methoden zum Clipping, auch eine unserer schiebe()Funktion entsprechende Methode wäre nützlich. Wenn Sie wollen, könnten Sie auch eine Methode wechsleHintergrundFarbe() oder geheDreiMalImKreis() definieren. Lösung zu 19: Angenommen, im head der Seite ist der XElement-Konstruktor und die StyleDefinition für die Regentropfen eingebaut. Dann schreiben wir im body die 50 Tropfen:
258
Lösungen for (i=0;i<50;i++) { var str = '
'; document.write(str); }
Eine init()-Funktion erzeugt die XElement-Instanzen und verteilt die Tropfen zufällig auf dem Bildschirm. Dazu wird als Erstes die Fenstergröße ausgemessen: function init() { if (window.innerWidth) { fensterBreite = innerWidth; fensterHoehe = innerHeight; } else { fensterBreite = document.body.clientWidth; fensterHoehe = document.body.clientHeight; } tropfen = new Array(); for (i=0;i<50;i++) { tropfen[i] = new XElement("tropfen"+i); tropfen[i].gehNach(Math.random()*(fensterBreite-15), Math.random()*(fensterHoehe-15)); } setInterval("regne()",40); } window.onload = init;
Ich habe ein bisschen Abstand zum rechten und unteren Fensterrand gelassen, damit keine Regentropfen darüber hinausstehen und die Scrollleiste angezeigt wird. Wie in init() festgelegt, wird von jetzt an alle 40 Millisekunden die Funktion regne() aufgerufen. Darin schieben wir alle Tropfen ein Stück nach unten. Die, die schon ganz unten sind, verteilen wir wieder an der Fensteroberkante. function regne(){ for (i=0;i
259
10 DHTML: Cross-Browser-Strategien Lösung zu 20: Ausführliche Kommentare zum folgenden Quelltext entnehmen Sie den Tipps zu dieser Übung.
<style type="text/css"> a.haupt { text-decoration:none; font-family:Verdana,Arial,sans-serif; font-size:14pt; } a.sub { text-decoration:none; font-family:Verdana,Arial,sans-serif; font-size:12pt; } <script language="JavaScript">
260
Lösungen } } } function klapp(nr) { if (subMenu[nr].sichtbar) subMenu[nr].verstecke(); else subMenu[nr].zeige(); ordnen(); } // -->
261
10 DHTML: Cross-Browser-Strategien Zu verbessern wäre jetzt nur noch, dass das Ganze bei Browsern, die wie Netscape 3 und IE 3 zwar JavaScript, aber kein DHTML kennen, nicht zu Fehlermeldungen führt. Wie man dabei vorgeht, üben wir demnächst. Lösung zu 21: klappMenuZaehler = 0; function KlappMenu(x,y,hauptArray,subArrays) { this.nr = klappMenuZaehler++; // Referenz erzeugen: self["klappMenu"+this.nr] = this; this.hauptMenuPunkte = hauptArray.length; this.menuY = y; this.menuX = x; this.ordnen = KlappMenu_ordnen; this.klapp = KlappMenu_klapp; // Menü schreiben var str = ""; for (var i=0;i
'; // Untermenue: str+=''; } document.write(str); this.hauptMenu = new Array(); this.subMenu = new Array(); for (i=0;i