Hoch Inhalt dieses Kapitels:

Linkwerk
screeneXa

Erhältlich u.a. bei  Amazon.de

10

DHTML: Cross-Browser-Strategien

Von Wolfgang Schwarz

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.

Browser-Gruppen

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.

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 denen 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.

Browser-Erkennung

Die beiden Möglichkeiten zur Browser-Erkennung haben Sie bereits in Kapitel 3 kennen gelernt: Entweder man liest die Eigenschaften des navigator-Objekts 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)

Hinweis

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 Großteil (aber eben nicht alles) des Microsoft-Objektmodells verstehen, darunter die Collection document.all. Noch gefährlicher ist Opera 4.0, 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 userAgent-Eigenschaft 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

Browser-Umleitung

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:

Beispiel

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

Hinweis

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 Skript-Bereiche 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 script-Tags 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.

Skript-Verzweigung

Am besten verzweigt man deshalb in den Skript-Blöcken selbst je nach Browser auf die entsprechenden Anweisungen:

<script language="JavaScript1.2">
<!--
if (document.getElementById) {
   ... Anweisungen für W3C-Browser ...
}
else if (document.all) {
   ... Anweisungen für Internet Explorer ...
}
else if (document.layers) {
   ... Anweisungen für Netscape 4 ...
}
 else {
   ... Anweisungen für alle anderen ...
}
// -->
</script>

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 4.0 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

Beispiel

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 visibility-Eigenschaft auf "hidden" schaltet:

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-Browser-

Libraries

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-Browser-Objekte 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(); 

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.javascript-workshop.de/.)

Noch ein paar Grundregeln

Testen

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.

Tipp

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.

Nachdenken

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.

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.

10.2 Übungen

DHTML-Übungen mit mehr Interaktion zwischen Webseite und Besucher finden Sie im Kapitel "Erweitertes Event-Handling".

Uebung

leicht

1. Übung

Hier ist ein Stück aus einem HTML-Dokument:

<div id="vater"
   style="position:absolute; left:10px; top:10px">
<div id="sohn"
   style="position:absolute; left:0px; top:10px">
Nichts ist besser als nichts.
</div>
</div>

Wie spricht man das Element mit der ID sohn in Netscape 4 an, wie im Internet Explorer, und wie im W3C DOM?

Uebung

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?

Uebung

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?

Uebung

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?

Uebung

mittel

5. Übung

Gibt es zur vorigen Übung auch eine Lösung für Netscape 4?

Uebung

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.

Uebung

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.

Uebung

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.

Uebung

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.

Uebung

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

Uebung

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.

Uebung

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.

Uebung

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.

Uebung

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.

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. (Sie erinnern sich: Der Ausdruck this verweist 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.

Uebung

mittel

15. Übung

Angenommen, auf einer HTML-Seite befindet sich ein positioniertes div-Element:

<div id="grauschwarz"
   style="position:absolute; left:100; top:200">
ein Gedanke
</div>

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?

alert(grauschwarzEbene.y);
grauschwarzeEbene.y = 50;
alert(grauschwarzeEbene.y);

Uebung

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.

Uebung

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;
   }
}

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. Kap 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.

Uebung

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().

Uebung

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.

Uebung

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.

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.

Uebung

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:

<body>
...
<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);
</script>
</body>

Diese Anweisungen sollen ausreichen, um das gewünschte Menü, voll funktionsfähig, an der gewünschten Position auf der Seite erscheinen zu lassen.

Uebung

mittel

Übung 22

Eines haben wir bislang sowohl bei den Cross-Browser-Funktionen als auch bei den Methoden von XElement übersehen: Layer können ineinander verschachtelt sein:

<div id="mutter" style="position:absolute">
   <div id="tochter" style="position:absolute">
   </div>
</div>

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?

Uebung

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).

Uebung

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.

<html>
<head>
<script>
function menuEinAus(){
 ...
}
function zeige(seite){
 ...
}
</script>
</head>
<body>

<!-- Hier ist ein Link, der ein Menü ein-/ausblendet -->
<div id="menuSchalter"
   style="position:absolute; left:10px; top:200px">
<a href="javascript:menuEinAus()">Menü anzeigen</a>
</div>

<!-- Das ist das Menü dazu. -->
<div id="menu"
   style="position:absolute; left:20; top:230;
   visibility:hidden">
<a href="javascript:zeige('eins.html')">eins</a><br>
<a href="javascript:zeige('zwei.html')">zwei</a><br>
<a href="javascript:zeige('drei.html')">drei</a><br>
</div>

<!-- Das ist der Haupt-Text der Seite -->
<div id="text" style="position:absolute; left:300; top:100">
Bla bla
</div>

</body>
</html>

10.3 Tipps

Tipp zu 4:

Tipps zu 5:

Tipp zu 6:

Tipp zu 7:

Tipps zu 9:

Tipps zu 10:

Tipps zu 12:

Tipp

Tipps zu 13:

Tipp zu 14:

Tipps zu 19:

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.

Tipp

Tipps zu 21:

Tipp zu 22:

Tipps zu 24:

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;

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:

<body>
<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";
      document.hilfsLayer.visibility = "visible";
   }
}
</script>

<div id="hilfsLayer"
   style="position:absolute; left:0px; top:0px">
</div>

<table border=1>
   <tr>
       <td>
           erste Zelle
       </td>
       <td><span id="zelle" style="position:relative">
           zweite Zelle
       </span></td>
   </tr>
</table>

<a href="javascript:aendern('bla bla bla')">
   Zelleninhalt ändern
</a>

</body>

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:

<td id="zelle" style="position:relative">
   zweite Zelle
</td>

Die Erfahrung zeigt aber, dass positionierte Tabellenzellen häufig Ärger machen. Der IE4 auf MacIntosh kommt damit zum Beispiel überhaupt nicht klar.

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.

Ganz ähnlich können Sie übrigens vorgehen, um einen Rollover-Effekt bei Text-Links mit Netscape 4 zu erreichen.

Lösung zu 6:

<body>
Die Zeit es ist:
<div id="uhr"
   style="position:absolute; left:10px; top:30px">
    
</div>

<script language="JavaScript1.2">
<!--
function tick() {
   // neues Date-Objekt auf aktuelle Zeit setzen:
   var jetzt = new Date();
   // Stunden, Minuten und Sekunden auslesen:
   var stunden = jetzt.getHours();
   var minuten = jetzt.getMinutes();
   var sekunden = jetzt.getSeconds();
   // eventuell noch eine 0 vorsetzen:
   if (stunden < 10) stunden = "0"+stunden;
   if (minuten < 10) minuten = "0"+minuten;
   if (sekunden <10) sekunden = "0"+sekunden;
   // aktuelle Zeitangabe zusammensetzen:
   var zeit = stunden+":"+minuten+":"+sekunden;

   // uhr-div überschreiben:
   if (document.getElementById) {
      var uhrEl = document.getElementById("uhr");
      uhrEl.firstChild.nodeValue = zeit;
   }
   else if (document.all) {
      document.all["uhr"].innerHTML = zeit;
   }
   else if (document.layers) {
      document.layers["uhr"].document.write(zeit);
      document.layers["uhr"].document.close();
   }
}
setInterval("tick()",1000);

// -->
</script>
</body>

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.

Hinweis

Hier zeigt sich, dass für viele Anwendungen die im DOM fehlende innerHTML-Eigenschaft 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) {
      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ßeinheit sein, also zum Beispiel "150px". Internet Explorer 5 und Netscape 6 akzeptieren aber 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];
}

Hinweis

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 div-Element:

<div id="inhalt" style="position:absolute">
   ... hier steht der ganze Inhalt ...
</div>

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

<a href="javascript:void(0)"
   onmousedown="laeuft=1; hoch(); return false"
   onmouseup="laeuft=0">hoch</a>
<br>
<a href="javascript:void(0)"
   onmousedown="laeuft=1; runter(); return false"
   onmouseup="laeuft=0">runter</a>

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);
}

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 Text-Links 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.

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;

   // 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 Style-Eigenschaft left des HTML-Elements.

Hinweis

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;
   }
   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 Style-Definition für die Regentropfen eingebaut. Dann schreiben wir im body die 50 Tropfen:

for (i=0;i<50;i++) {
   var str = '<div id="tropfen'+i+'" class="tropf"></div>';
   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 Sekunden 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<tropfen.length;i++){
      if (tropfen[i].y<fensterHoehe-40) {
         var neuX = tropfen[i].x-1;
         var neuY = tropfen[i].y+20;
      }
      else {
         var neuX = Math.random()*(fensterBreite-15);
         var neuY = -10;
      }
      tropfen[i].gehNach(neuX, neuY);
   }
}

Lösung zu 20:

Ausführliche Kommentare zum folgenden Quelltext entnehmen Sie den Tipps zu dieser Übung.

<html>
<head><title></title>
<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;
}
</style>

<script language="JavaScript">
<!--

hauptMenuPunkte = 3;
menuY = 100;
menuX = 20;

function XElement(id) {
   [...]
}

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();
}

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;
      }
   }
}

function klapp(nr) {
   if (subMenu[nr].sichtbar) subMenu[nr].verstecke();
   else subMenu[nr].zeige();
   ordnen();
}

// -->
</script>
</head>

<body onload="init()">

<div id="menu1" style="position:absolute">
   <a href="javascript:void(0)" class="haupt"
      onclick="klapp(1); return false">Früchte</a>
</div>
<div id="menu1sub" style="position:absolute">
   <a href="apfel.html" class="sub"
      onclick="zeige(this.href); return false">Apfel</a>
   <br>
   <a href="birne.html" class="sub"
      onclick="zeige(this.href); return false">Birne</a>
   <br>
   <a href="kirsche.html" class="sub"
      onclick="zeige(this.href); return false">Kirsche</a>
   <br>
</div>
<div id="menu2" style="position:absolute">
   <a href="javascript:void(0)" class="haupt"
      onclick="klapp(2); return false">Tonarten</a>
</div>
<div id="menu2sub" style="position:absolute">
   <a href="amoll.html" class="sub"
      onclick="zeige(this.href); return false">a Moll</a>
   <br>

[usw.]

</div>
</body>
</html>

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<this.hauptMenuPunkte;i++) {
      // Hauptmenue-Punkt:
      str+='<div id="menu'+this.nr+i+'" class="abs">'
         + '<a href="javascript:void(0)" class="haupt" '
         + 'onclick="klappMenu'+this.nr+'.klapp('+i+'); '
         + 'return false">' + hauptArray[i] + '</a></div>';
      // Untermenue:
      str+='<div id="menu'+this.nr+i+'sub" class="abs">';
      for (var j=0;j<subArrays[i].length;j+=2) {
         str +='<a href="'+subArrays[i][j+1]+'" class="sub"'
             + 'onclick="klappMenu'+this.nr
             + '.zeige(this.href) ; return false">'
             + subArrays[i][j] + '</a><br>';
      }
      str+='</div>';
   }
   document.write(str);

   this.hauptMenu = new Array();
   this.subMenu = new Array();
   for (i=0;i<this.hauptMenuPunkte;i++) {
      this.hauptMenu[i] = new XElement("menu"+this.nr+i);
      this.subMenu[i] =
         new XElement("menu"+this.nr+i+"sub");
      this.subMenu[i].verstecke();
   }
   this.ordnen();
}

function KlappMenu_ordnen() {
   var posY = this.menuY;
   for (i=0;i<this.hauptMenuPunkte;i++) {
      this.hauptMenu[i].gehNach(this.menuX,posY);
      posY+=this.hauptMenu[i].hoehe;
      if (this.subMenu[i].sichtbar) {
         this.subMenu[i].gehNach(this.menuX+20,posY);
         posY+=this.subMenu[i].hoehe;
      }
   }
}

function KlappMenu_klapp(nr) {
   if (this.subMenu[nr].sichtbar)
      this.subMenu[nr].verstecke();
   else this.subMenu[nr].zeige();
   this.ordnen();
}

Mit Hilfe dieses Konstruktors können Sie jetzt ohne großen Aufwand 27 Klappmenüs auf Ihrer Seite platzieren, wenn es Ihnen gefällt.

Lösung zu 22:

Der parent-Layer wird am besten als String (und nicht als Referenz) übergeben, denn sonst bräuchte man ja beim Aufruf wieder Browser-Verzweigungen. Ein Aufruf sieht also etwa so aus:

Xtochter = new XElement("tochter", "document.mutter");

Wie gehen wir jetzt in der Konstruktorfunktion damit um? Es muss lediglich die Definition von el geändert werden. Am einfachsten geht das mit eval:

function XElement(id, parent) {
   if (document.layers) {
      if (parent) this.el = eval(parent + ".document." +id);
      else this.el = document.layers[id];
   }
   ... der Rest bleibt wie gehabt...
}

eval führt den JavaScript-Code aus, der ihm als String übergeben wird. Dabei wird intern ein neuer Skript-Block erzeugt, was die Ausführung leider etwas verzögert und fehleranfällig macht. Um ohne eval auszukommen, könnten wir den parent-String durchsuchen und daraus von Hand die Referenz auf den übergeordneten Layer zusammensetzen:

function XElement(id, parent) {
   if (document.layers) {
      if (parent) {
         var vorfahren =
            parent.split(/document\.|\.document\./);
         var par = window;
         for (var i=1; i<vorfahren.length; i++)
            par = par.document.layers[vorfahren[i]];
         this.el = par.document.layers[id];
      }
      else this.el = document.layers[id];
   }
   ... der Rest bleibt wie gehabt...
}

Hier wird vorausgesetzt, dass die übergeordneten Layer immer als "document.layerID" übergeben werden und nicht etwa als "document.layers.layerID" oder "document.layers['layerID']".

Eine andere, und vermutlich die schönste Lösung der Aufgabe bestünde darin, überhaupt keinen zweiten Parameter zu benutzen und stattdessen die Konstruktorfunktion nach dem Ziel-Element suchen zu lassen. Dazu müsste sie rekursiv die document.layers-Hierarchie durchwandern.

Lösung zu 23:

Eine einfache Lösung ist diese:

<script>
<!--
// DHTML-Browser abfragen:
var n4 = document.layers;
var w3c= (document.getElementById &&
   navigator.userAgent.toLowerCase().indexOf("opera")<0);
var ieWin = (document.all &&
   navigator.userAgent.toLowerCase().indexOf("mac")<0);

// DHTML-Browser umleiten:
if (n4 || w3c || ieWin) location.replace("dhtmlSeite.html");

// Andere Browser umleiten:
else location.href = "alternativSeite.html";
// -->
</script>

<a href="alternativSeite.html">bitte eintreten</a>

Bei Browsern ohne JavaScript muss der Link "bitte eintreten" geklickt werden, um zur Alternativseite zu gelangen. Dies lässt sich umgehen, indem wir die Umleitungsseite selbst zur Alternativseite machen:

<html>
<head>
<title>Willkommen</title>
<script>
<!--
var n4 = document.layers;
var w3c= (document.getElementById &&
   navigator.userAgent.toLowerCase().indexOf("opera")<0);
var ieWin = (document.all &&
   navigator.userAgent.toLowerCase().indexOf("mac")<0);
if (n4 || w3c || ieWin) location.replace("dhtmlSeite.html");
// -->
</script>
</head>
<body>

... hier steht der Text für DHTML-unkundige Browser...

</body>
</html>

Eine Browser-Umleitung ist aber auch ohne JavaScript möglich, und zwar mit Hilfe dieses meta-Tags:

<meta http-equiv="refresh"
   content="10;URL=irgendwohin.html">

Die erste Angabe im content-Attribut bestimmt die Zeit (hier 10 Sekunden), nach der die Seite mit der angegebenen Adresse geladen wird.

Damit können wir nun auch JavaScript-unkundige Browser umleiten. Eine weitere Lösung der Aufgabe sieht also so aus:

<html>
<head>
<title>Willkommen</title>
<script>
<!--
var n4 = document.layers;
var w3c= (document.getElementById &&
   navigator.userAgent.toLowerCase().indexOf("opera")<0);
var ieWin = (document.all &&
   navigator.userAgent.toLowerCase().indexOf("mac")<0);
if (n4 || w3c || ieWin) location.replace("dhtmlSeite.html");
// -->
</script>
<meta http-equiv="refresh"
   content="0;URL= alternativSeite.html">
</head>
<body>
<a href="alternativSeite.html">Hereinspaziert</a>
</body>
</html>

Ich habe den Link zur Alternativseite vorsichtshalber nicht weggenommen. Schließlich könnte es ja Browser geben, die nicht nur kein JavaScript kennen, sondern auch mit dem Meta-Tag nichts anzufangen wissen.

Übrigens sollte wegen eines Bugs in Netscape 4 die JavaScript-Umleitung im Quelltext immer oberhalb des Meta-Tags stehen.

Lösung zu 24:

Die DHTML-Funktionen sollen vor alten Browsern versteckt werden. Eine Möglichkeit, dies ohne Fehlermeldungen zu erreichen, ist diese:

<script>
<!--
function menuEinAus(){}
function zeige(){}
// -->
</script>

<script language="JavaScript1.2">
<!--
function menuEinAus(){
 ...
}
function zeige(seite){
 ...
}
// -->
</script>

Die Funktionen werden einfach zweimal definiert, zuerst als leere Dummy-Funktionen für alle Browser, dann werden sie für JavaScript1.2-kompatible Browser mit den richtigen Funktionen überschrieben.

Da aber Opera zum Beispiel auch als JavaScript1.2 gekennzeichnete Anweisungen ausführt, ist es sicherer, statt der ganzen Funktionsdefinition nur den Funktionsinhalt zu verstecken:

<script>
<!--
if (document.layers || document.all ||
   document.documentElement) // Browser kann DHTML
   var dhtml = true;
else dhtml = false;

function menuEinAus(){
   if (!dhtml) return;
   ...
}
function zeige(seite){
   if (!dhtml) return;
   ...
}
// -->
</script>

Diesmal habe ich nicht den UserAgent abgefragt, sondern einfach ein Objekt gesucht, das die "richtigen" W3C-Browser kennen, Opera 4.0 aber nicht: document.documentElement.

Im body empfehlen sich drei Änderungen:

Erstens bringen wir den Block mit dem Inhalt an den Anfang der Seite. Für DHTML-fähige Browser wird die Position ohnehin über CSS definiert, deshalb können wir die divs im Quelltext nach Belieben umsortieren.

Als Zweites wollen wir für Nicht-DHTML-Browser den Link "Menü anzeigen" loswerden. Das Menü ist schließlich in diesen Browsern ohnehin immer zu sehen. Wir schreiben den Link deshalb für die "richtigen" Browser per JavaScript ins Dokument.

Schließlich sollen die Links im Menü auch ohne JavaScript funktionieren. Dazu ändern wir die href-Eigenschaft in die Adresse der entsprechende Seite und rufen die Funktion zeige() bei onclick auf. Der onclick-Handler gibt false zurück, um den Link zu deaktivieren.

Der body-Bereich sieht jetzt so aus:

<body>
<div id="text" style="position:absolute; left:300; top:100">
Bla bla
</div>

<div id="menu"
   style="position:absolute; left:20; top:230;
   visibility:hidden">
<a href="eins.html"
   onClick="zeige('eins.html'); return false">eins</a>
<br>
<a href="zwei.html"
   onClick="zeige('zwei.html'); return false">zwei</a>
<br>
<a href="drei.html"
   onClick="zeige('drei.html'); return false ">drei</a>
<br>
</div>

<script language="JavaScript1.2">
<!--
var str = '<div id="menuSchalter"';
str += 'style="position:absolute; left:10; top:200">';
str += '<a href="javascript:menuEinAus()">';
str += 'Menü anzeigen</a></div>';
document.write(str);
// -->
</script>
</body>

10.5 Anhang 1: Eine Checkliste für Netscape 4

Die DHTML-Implementation in Netscape 4 weist einige Schwächen auf, die sich auf mitunter sehr eigenartige Weise zeigen und oft nur schwer zu debuggen sind. Wenn Sie also auf seltsame Effekte stoßen oder Ihnen Ihre Seite irgendwie instabil vorkommt, dann gehen Sie die folgende Liste durch.

Sollte Ihr Netscape Navigator einmal nicht mehr reagieren, dann schließen Sie ihn und kicken ihn anschließend auch noch aus dem Task-Manager Ihres Betriebssystems (unter Windows über (Strg) (Alt) (Entf)). Erst danach können Sie ihn neu starten.

10.6 Anhang 2: Kleine Cross-Browser-Referenz

Die folgende Tabelle zeigt, wo sich in den verschiedenen Objektmodellen einige Werte verstecken, die man häufig für DHTML-Anwendungen benötigt.

Netscape 4Netscape 6IE 4/5
Fenster-Innengröße:
window.innerWidth
window.innerHeight
document.body.clientWidth
document.body.clientHeight
Dokumentgröße:
document.width
document.height
document.body.scrollWidth
document.body.scrollHeight
Scrollposition:
window.pageXOffset
window.pageYOffset
document.body.scrollLeft
document.body.scrollTop
Position eines Elements (relativ zum Dokument):
nur bei Layern:*
Layer.pageX
Layer.pageY
Sie müssen Ihre eigene Funktion schreiben:
{
   var x=0;
   while (elem!=null){
      x+=elem.offsetTop;
      elem=elem.offsetParent;
   }
   return x;
}

für getY() ersetzen Sie offsetTop durch offsetLeft.
Größe eines Elements:
nur bei Layern:*
Layer.document.width
Layer.document.height
Element.offsetWidth
Element.offsetHeight
Inhalt eines Elements:
-Element.innerHTML

Beachten Sie, dass die Werte von document.body während des Seitenaufbaus im head noch nicht zur Verfügung stehen.

* Um die Werte von einem anderen Element zu bekommen, stellen Sie dieses einfach in einen relativ positionierten Layer.

Linkwerk
screeneXa