|
|
|
|
JavaScript - Das umfassende ReferenzwerkDavid Flanagan Übersetzung der 4. engl. Auflage 2. Auflage, Juni 2002 ISBN 3-89721-330-3 1024 Seiten |
Ein Document Object Model (DOM) ist eine API (Application Programming Interface), die ein Dokument (beispielweise ein HTML-Dokument) repräsentiert sowie den lesenden und schreibenden Zugriff auf die Bestandteile des Dokuments (wie z. B. HTML-Tags und Strings mit Text) ermöglicht. JavaScript-fähige Webbrowser haben immer ein Document Object Model implementiert. Beispielsweise kann das DOM eines Webbrowsers festlegen, daß auf die in einem HTML-Dokument enthaltenen Formulare über das Array forms[] des Document-Objekts zugegriffen werden kann.
In diesem Kapitel werden wir das W3C DOM betrachten, das ein vom World Wide Web Consortium standardisiertes und (zumindest teilweise) von den Browsern Netscape 6 und Internet Explorer 5 und 6 implementiertes Document Object Model definiert. Dieser DOM-Standard1 ist eine vollständig ausgeprägte Obermenge des DOM traditioneller Webbrowser. Es stellt HTML- (und XML-) Dokumente in einer Baumstruktur dar und definiert Eigenschaften und Methoden, mit denen der Baum durchlaufen sowie die Knoten betrachtet und modifiziert werden können. Andere Teile des Standards spezifizieren Techniken für die Definition von Event-Handlern für die Knoten eines Dokuments, das Arbeiten mit Stylesheets und die Bearbeitung von aufeinanderfolgenden Bereichen eines Dokuments.
Dieses Kapitel beginnt mit einem Überblick über den DOM-Standard und beschreibt anschließend den Kern dieses Standards zum Arbeiten mit HTML-Dokumenten. Daran schließen sich einige kurze Abschnitte an, die die DOM-ähnlichen Merkmale der Browser Internet Explorer 4 und Netscape 4 beschreiben. Das Kapitel endet mit einem Überblick über zwei optionale Bestandteile des DOM-Standards, die eng mit dem Kern zusammenhängen. In separaten Kapiteln werden die erweiterten DOM-Merkmale für das Arbeiten mit Stylesheets und Events behandelt.
Die DOM-API ist nicht sehr kompliziert; bevor wir jedoch mit der Erläuterung beginnen können, wie mit dem DOM programmiert werden kann, stellen wir einige Grundlagen der DOM-Architektur vor, die Sie kennen sollten.
HTML-Dokumente besitzen eine hierarchische Struktur, die im DOM als Baumstruktur repräsentiert wird. Die Knoten des Baums stellen die verschiedenen Arten von Inhalt in einem Dokument dar. Die Repräsentation eines HTML-Dokuments als Baum enthält in erster Linie Knoten, die verschiedene Elemente oder Tags wie <body> oder <p> sowie Knoten mit dem eigentlichen Text des Dokuments als String darstellen. Ein HTML-Dokument kann außerdem Knoten enthalten, die HTML-Kommentare darstellen.2 Betrachten Sie das folgende einfache HTML-Dokument:
<html>
<head>
<title>Beispieldokument</title>
</head>
<body>
<h1>Ein HTML-Dokument</h1>
<p>Dies ist ein <i>einfaches</i> Dokument.
</body>
</html>
Die DOM-Darstellung dieses Dokuments ist der in Abbildung 17-1 abgebildete Baum.
Wenn Sie noch nicht mit Baumstrukturen in der Programmierung vertraut sind, hilft es Ihnen möglicherweise, wenn Sie wissen, daß die Begrifflichkeit an Stammbäume angelehnt ist. Der Knoten unmittelbar über einem anderen Knoten ist der Elternknoten (oder Vater) dieses Knotens. Die Knoten direkt unterhalb eines anderen Knotens sind seine Kinder. Knoten derselben Ebene, die den gleichen Elternknoten besitzen, sind Geschwister. Die Menge der Knoten in einer beliebigen Ebene unterhalb eines Knotens sind seine Nachfahren. Der Eltern-, Großeltern- und alle weiteren Knoten oberhalb eines Knotens sind seine Vorfahren.
Die in Abbildung 17-1 abgebildete Baumstruktur des DOM wird als ein Baum mit verschiedenen Typen von Knotenobjekten repräsentiert. Das Interface3 Node definiert Eigenschaften und Methoden, mit denen der Baum durchlaufen und manipuliert werden kann. Die Eigenschaft childNodes eines Node-Objekts gibt eine Liste der Kinder des Knotens zurück; die Eigenschaften firstChild, lastChild, nextSibling, previousSibling und parentNode ermöglichen es, den Baum zu durchlaufen. Mit Methoden wie appendChild(), removeChild(), replaceChild() und insertBefore() können Sie dem Baum Knoten hinzufügen bzw. Knoten aus dem Baum entfernen. Weiter unten in diesem Kapitel werden wir Beispiele sehen, in denen diese Eigenschaften und Methoden benutzt werden.
Verschiedene Knotentypen im Dokumentenbaum werden durch verschiedene Unterinterfaces von Node dargestellt. Jedes Node-Objekt besitzt eine Eigenschaft nodeType, die die Art des Knotens angibt. Wenn die Eigenschaft nodeType eines Knotens beispielsweise die Konstante Node.ELEMENT_NODE als Wert besitzt, wissen Sie, daß das Node-Objekt auch ein Element-Objekt ist und Sie daher alle vom Interface Element definierten Methoden und Eigenschaften für dieses Element verwenden können. In Tabelle 17-1 sind die am häufigsten in HTML-Dokumenten benutzten Knotentypen mit dem ihnen entsprechenden Wert des nodeType aufgeführt.
Das Node-Objekt an der Wurzel des DOM-Baums ist ein Document-Objekt. Die Eigenschaft documentElement dieses Objekts verweist auf ein Element-Objekt, das das Wurzelelement des Dokuments darstellt. Bei HTML-Dokumenten ist dies das Tag <html>, das entweder explizit oder implizit im Dokument enthalten ist. (Der Document-Knoten kann zusätzlich zum Wurzelelement weitere Kinder besitzen - wie beispielsweise Comment-Knoten.) Der größte Teil eines DOM-Baums besteht aus Element-Objekten, die Tags wie <html> und <i> darstellen, sowie aus Text-Objekten, die Strings mit Text enthalten. Wenn der Parser bei der Analyse des Dokuments Kommentare nicht entfernt, sind diese im DOM-Baum als Comment-Objekte repräsentiert. Abbildung 17-2 zeigt einen Ausschnitt aus der Klassenhierarchie für diese und andere Interfaces des DOM-Kerns.
Die Attribute eines Elements (wie beispielsweise die Attribute src und width eines <img>-Tags) können mit den Methoden getAttribute(), setAttribute() und removeAttribute() des Element-Interface abgefragt, gesetzt und gelöscht werden.
Eine andere, umständlichere Art, mit Attributen zu arbeiten, besteht in der Verwendung der Methode getAttributeNode(), die ein Attr-Objekt zurückgibt. Dieses Objekt repräsentiert ein Attribut und dessen Wert. (Ein Grund dafür, diese umständlichere Technik zu verwenden, besteht darin, daß das Interface Attr eine Eigenschaft specified definiert, mit der Sie herausfinden können, ob das Attribut ausdrücklich im Dokument angegeben wurde oder ob es sich bei dem Wert um einen voreingestellten Wert handelt.) Das Interface Attr ist in Abbildung 17-2 dargestellt; es ist ein Knotentyp. Beachten Sie jedoch, daß Attr-Objekte nicht im Array childNodes[] eines Elements enthalten sind und nicht direkt Teil des Dokumentbaums in der Art sind, wie Element- und Text-Knoten dies sind. Die DOM-Spezifikation ermöglicht den Zugriff auf Attr-Knoten durch das Array attributes[] des Node-Interface; der Internet Explorer von Microsoft definiert ein davon verschiedenes, inkompatibles Array attributes[], so daß dieses Array nicht in portablen Anwendungen genutzt werden kann.
Der DOM-Standard wurde für die Verwendung sowohl mit XML- als auch mit HTML-Dokumenten entworfen. Die API des DOM-Kerns - die Interfaces Node, Element, Document und andere - ist recht generisch und ist auf beide Arten von Dokumenten anwendbar. Der DOM-Standard enthält darüber hinaus Interfaces, die speziell für HTML-Dokumente entworfen wurden. Wie Sie in Abbildung 17-2 gesehen haben, ist HTMLDocument ein HTML-spezifisches Unterinterface von Document und HTMLElement ein HTML-spezifisches Unterinterface von Element. Außerdem definiert das DOM Tag-spezifische Interfaces für viele HTML-Elemente - wie HTMLBodyElement und HTMLTitleElement. Diese definieren üblicherweise eine Menge von Eigenschaften, die die Attribute des HTML-Tags widerspiegeln.
Das Interface HTMLDocument definiert verschiedene Eigenschaften und Methoden für Dokumente, die bereits vor der Standardisierung durch das W3C von Browsern unterstützt wurden. Dazu gehören die Eigenschaft location, das Array forms[] und die Methode write(), die in den Kapiteln 13, 14 und 15 beschrieben wurden.
Das Interface HTMLElement definiert die Eigenschaften id, style, title, lang, dir und className. Sie gestatten einen bequemen Zugriff auf die Werte der Attribute id, style, title, lang, dir und class, die jedes HTML-Tag besitzen kann. Einige HTML-Tags akzeptieren keine anderen als diese sechs Attribute und werden daher vollständig vom Interface HTMLElement dargestellt. Diese Tags sind in Tabelle 17-2 aufgeführt.
|
<abbr> |
<acronym> |
<address> |
<b> |
<bdo> |
|
<big> |
<center> |
<cite> |
<code> |
<dd> |
|
<dfn> |
<dt> |
<em> |
<i> |
<kbd> |
|
<noframes> |
<noscript> |
<s> |
<samp> |
<small> |
|
<span> |
<strike> |
<strong> |
<sub> |
<sup> |
|
<tt> |
<u> |
<var> |
|
|
Alle anderen HTML-Tags besitzen entsprechende Interfaces, die im HTML-Teil der DOM-Spezifikation definiert sind. Bei vielen HTML-Tags stellen diese Interfaces lediglich eine Menge von Eigenschaften bereit, die ihre HTML-Attribute widerspiegeln. Beispielsweise gibt es für das Tag <ul> ein entsprechendes Interface HTMLUListElement und für das Tag <body> ein entsprechendes Interface HTMLBodyElement. Da diese Interfaces lediglich Eigenschaften definieren, die im HTML-Standard enthalten sind, sind sie in diesem Buch nicht ausführlich dokumentiert. Sie können davon ausgehen, daß das HTMLElement-Objekt, das ein bestimmtes HTML-Tag repräsentiert, eine Eigenschaft für jedes der Standardattribute dieses Tags besitzt (beachten Sie dabei aber die Namenskonventionen, die im nächsten Abschnitt beschrieben werden). Der DOM-Standard definiert Eigenschaften für die HTML-Attribute, um Skript-Programmierern die Arbeit zu erleichtern. Die allgemeinere (und vermutlich bevorzugte) Methode, Attributwerte abzufragen und zu setzen, besteht in der Verwendung der Methoden getAttribute() und setAttribute() des Element-Objekts.
Einige der im HTML-DOM definierten Interfaces besitzen zusätzliche Eigenschaften oder Methoden. Beispielsweise definiert das Interface HTMLInputElement die Methoden focus() und blur(), das Interface HTMLFormElement die Methoden submit() und reset() und eine Eigenschaft length. Solche Methoden und Eigenschaften stammen üblicherweise aus der Zeit vor der DOM-Standardisierung und sind aus Gründen der Abwärtskompatibilität in den DOM-Standard aufgenommen worden. Diese Interfaces werden in der W3C DOM-Referenz dokumentiert. In der Referenz zu JavaScript auf der Clientseite finden Sie meist auch Informationen über die übliche Verwendungspraxis dieser Interfaces. Normalerweise finden Sie diese Informationen dort unter einem Namen, der ebenfalls aus der Zeit vor der DOM-Standardisierung stammt - beispielsweise finden Sie Informationen zu HTMLFormElement und HTMLInputElement in der Referenz zu JavaScript auf der Clientseite unter »Form« und »Input«.
Wenn Sie mit den HTML-spezifischen Teilen des DOM-Standards arbeiten, sollten Sie einige einfache Namenskonventionen beachten. Eigenschaften von HTML-spezifischen Interfaces beginnen mit kleinen Buchstaben. Wenn der Name der Eigenschaft aus mehreren Wörtern besteht, werden die Anfangsbuchstaben des zweiten Worts und der folgenden Wörter groß geschrieben. Das Attribut maxlength des <input>-Tags wird beispielsweise auf die Eigenschaft maxLength von HTMLInputElement abgebildet.
Wenn der Name eines HTML-Attributs mit einem Schlüsselwort von JavaScript in Konflikt gerät, wird der String »html« vorangestellt, um den Konflikt zu vermeiden. Das Attribut for des <label>-Tags wird daher beispielsweise auf die Eigenschaft htmlFor von HTMLLabelElement abgebildet. Eine Ausnahme von dieser Regel stellt das Attribut class dar (das für jedes HTML-Element angegeben werden kann); es wird auf die Eigenschaft className von HTMLElement abgebildet.4
Es gibt zwei Versionen (sog. »Level«) des DOM-Standards. DOM Level 1 wurde im Oktober 1998 standardisiert. Diese Version definiert die Interfaces des DOM-Kerns wie Node, Element, Attr und Document sowie verschiedene HTML-spezifische Interfaces. DOM Level 2 wurde im November 2000 standardisiert.5 Neben einigen Aktualisierungen der Kern-Interfaces ist diese neue Version des DOM stark erweitert worden: um Standard-APIs für das Arbeiten mit Events in Dokumenten und CSS-Stylesheets sowie um zusätzliche Werkzeuge zum Arbeiten mit Bereichen von Dokumenten. Während dieses Buch geschrieben wurde, arbeitete die DOM Working Group des W3C gerade an der Standardisierung des DOM Level 3. Mitunter wird Ihnen auch ein Hinweis auf das DOM Level 0 begegnen. Dieser Begriff bezieht sich nicht auf einen formalen Standard; er verweist informell auf die gemeinsamen Merkmale der Document Object Models für HTML, die vor der Standardisierung durch das W3C von Netscape und dem Internet Explorer implementiert waren.
Im Level 2 wurde der DOM-Standard »modularisiert«. Das Kernmodul, das die grundlegende Baumstruktur eines Dokuments mit den Interfaces Document, Node, Element und Text (und anderen) definiert, ist das einzige Modul, das auf jeden Fall verwendet werden muß. Alle anderen Module sind optional und können, müssen aber nicht unterstützt werden; dies ist vom Bedarf der Implementation abhängig. Die DOM-Implementation eines Webbrowsers wird vermutlich das HTML-Modul umsetzen, da Webseiten in HTML geschrieben sind. Browser, die CSS-Stylesheets unterstützen, setzen üblicherweise die Module StyleSheets und CSS um, da (wie Sie in Kapitel 18 sehen werden) CSS-Styles eine wichtige Rolle bei der Programmierung von Dynamic HTML spielen. Sie erwarten vermutlich, daß die meisten Webbrowser das Events-Modul der DOM-Spezifikation umsetzen, da fast alle interessanten JavaScript-Programme die Möglichkeit zur Verarbeitung von Events benötigen. Leider wurde das Events-Modul erst kürzlich von der Spezifikation des DOM Level 2 definiert, so daß es derzeit noch keine breite Unterstützung genießt. Eine vollständige Liste aller Module des DOM Level 2 werden Sie im nächsten Abschnitt sehen.
Während ich diese Zeilen schreibe, erfüllt kein Browser den DOM-Standard vollständig. Die neuesten Versionen des Mozilla kommen ihm am nächsten - tatsächlich ist die vollständige Konformität zu DOM Level 2 ein Ziel des Mozilla-Projekts. Netscape 6.1 entpricht dem Standard in den wichtigsten Modulen von Level 2, während Netscape 6.0 angemessen arbeitet, aber Lücken in der Abdeckung besitzt. Der Internet Explorer 6 entspricht fast vollständig dem DOM Level 1 (mit mindestens einer störenden Ausnahme), unterstützt aber nur wenige der Level-2-Module - insbesondere nicht das Events-Modul, das Gegenstand von Kapitel 19 ist. Die Internet Explorer-Versionen 5 und 5.5 weisen substantielle Lücken in der Konformität auf, unterstützen aber die wichtigsten Methoden des DOM Level 1 so weit, daß die meisten Beispiele dieses Kapitels in ihnen ausgeführt werden können. Die Macintosh-Version des Internet Explorer 5 unterstützt das DOM erheblich besser als die Windows-Version des Internet Explorer 5.
Außer Mozilla, Netscape und dem Internet Explorer unterstützen verschiedene andere Browser das DOM zumindest teilweise. Die Anzahl der verfügbaren Browser ist jedoch zu groß und die Änderungsrate in bezug auf die Unterstützung von Standards zu hoch geworden, als daß dieses Buch auch nur versuchen könnte, definitive Aussagen darüber zu machen, welcher Browser welche DOM-Merkmale im einzelnen unterstützt. Daher werden Sie weitere Quellen heranziehen müssen, wenn Sie feststellen müssen, inwieweit die DOM-Implementation eines bestimmten Webbrowsers dem Standard entspricht.
Eine Quelle für solche Informationen ist die Implementation selbst. In einer standardkonformen Implementation verweist die Eigenschaft implementation des Document-Objekts auf ein DOMImplementation-Objekt, das eine Methode namens hasFeature() besitzt. Wenn diese Methode existiert, können Sie sie verwenden, um eine Implementation daraufhin zu prüfen, ob sie ein bestimmtes Merkmal (oder ein Modul) des DOM-Standards unterstützt. Um beispielsweise festzustellen, ob die DOM-Implementation in einem Webbrowser die grundlegenden Interfaces des DOM Level 1 zum Arbeiten mit HTML-Dokumenten unterstützt, können Sie die folgenden Programmzeilen verwenden:
if (document.implementation &&
document.implementation.hasFeature &&
document.implementation.hasFeature("html", "1.0")) {
// Der Browser behauptet, den Kern und das HTML-Interface von Level 1 zu unterstützen
}
Die Methode hasFeature() nimmt zwei Argumente entgegen: Das erste ist der Name des zu überprüfenden Merkmals, das zweite eine Versionsnummer, die als String angegeben wird. Die Methode gibt true zurück, wenn die angegebene Version des angegebenen Merkmals unterstützt wird. Tabelle 17-3 führt die Paare aus Namen und Versionsnummer auf, die von den Standards DOM Level 1 und Level 2 definiert werden. Beachten Sie, daß bei den Namen der Merkmale die Groß- und Kleinschreibung nicht von Bedeutung ist: Sie können sie schreiben, wie Sie möchten. Die vierte Spalte der Tabelle gibt an, welche anderen Merkmale benötigt werden, damit ein Merkmal unterstützt werden kann. Wenn die Methode also den Wert true zurückgibt, impliziert dies, daß die dort angegebenen Merkmale ebenfalls unterstützt werden. Wenn beispielsweise hasFeature() anzeigt, daß das Modul MouseEvents unterstützt wird, impliziert dies, daß UIEvents und damit auch die Module Events, Views und Core unterstützt werden.
Im Internet Explorer 6 (unter Windows) gibt hasFeature() den Wert true nur für das Merkmal HTML und Version 1.0 zurück. Er gibt nicht vor, zu irgendeinem der anderen in Tabelle 17-3 aufgeführten Merkmale konform zu sein (obwohl er tatsächlich die häufigsten Arten der Verwendung des CSS2-Moduls unterstützt, wie Sie in Kapitel 18 sehen werden). Im Netscape 6.1 gibt hasFeature() den Wert true für die meisten Namen und Versionsnummern von Merkmalen zurück - mit der bemerkenswerten Ausnahme von Traversal und MutationEvents. Die Methode gibt für die Merkmale Core und CSS2 in der Version 2.0 false zurück, um anzuzeigen, daß die Unterstützung nicht vollständig ist (obwohl sie recht gut ist).
Dieses Buch dokumentiert die Interfaces, aus denen die einzelnen DOM-Module in Tabelle 17-3 bestehen. Die Module Core, HTML, Traversal und Range werden in diesem Kapitel beschrieben, die Module StyleSheets, CSS und CSS2 in Kapitel 18 und die verschiedenen Event-Module (außer MutationEvents) in Kapitel 19. Der Referenzteil zum DOM enthält eine vollständige Dokumentation aller Module.
Die Methode hasFeature() ist nicht uneingeschränkt zuverlässig. Wie bereits gesagt wurde, behauptet der Internet Explorer 6, konform zu den HTML-Merkmalen des Level 1 zu sein, obwohl es tatsächlich einige Probleme mit der Konformität gibt. Andererseits meldet der Netscape 6.1, daß er nicht zum Merkmal Level 2 Core konform ist, obwohl er tatsächlich weitestgehend dem Standard entspricht. In beiden Fällen benötigen Sie genauere Informationen darüber, an welchen Stellen was dem Standard entspricht bzw. von ihm abweicht. Genau diese Art von Informationen ist jedoch zu umfangreich und ändert sich zu schnell, um sie in ein gedrucktes Buch zu binden.
Wenn Sie aktiv Webseiten entwickeln, werden Sie vermutlich bereits wissen oder bald entdecken, inwieweit Ihr eigener Browser dem Standard entspricht. Im Web finden Sie ebenfalls Quellen, die Ihnen helfen können. Die wichtigste Entwicklung ist die einer Testsuite für DOM-Implementationen auf Open Source-Basis, an der das W3C (zusammen mit dem amerikanischen National Institute of Standards and Technology) arbeitet. Während dieses Buch geschrieben wurde, kamen diese Bemühungen gerade in Gang; sie werden sich wohl als unschätzbare Quelle für ein exaktes Testen auf Konformität von DOM-Implementationen erweisen. Nähere Informationen finden Sie unter http://www.w3c.org/DOM/Test/.
Die Mozilla-Organisation hat Testsuites für eine Vielzahl von Standards - u. a. auch für DOM Level 1 (verfügbar unter http://www.mozilla.org/quality/browser_sc.html ). Netscape hat eine Testsuite veröffentlicht, die einige Tests für DOM Level 2 enthält (verfügbar unter http://developer.netscape.com/evangelism/tools/testsuites/ ). Netscape hat außerdem einen parteiischen (und veralteten) Vergleich der DOM-Konformität einer frühen Mozilla-Version mit dem Internet Explorer 5.5 veröffentlicht (verfügbar unter http://home.netscape.com/browsers/future/standards.html ). Schließlich können Sie Informationen über Kompatibilität und Konformität bei unabhängigen Stellen im Web finden. Eine beachtenswerte Site wird von Peter-Paul Koch gepflegt. Sie können einen Link zu seiner Kompatibilitätstabelle für das DOM auf seiner JavaScript-Einstiegsseite finden (http://www.xs4all.nl/~ppk/js/ ).
Da der Internet Explorer der am häufigsten verwendete Webbrowser ist, sind an dieser Stelle einige Bemerkungen darüber angebracht, inwieweit dieser Browser den DOM-Spezifikationen entspricht. Seit der Version 5 werden die Kern- und HTML-Merkmale des Level 1 hinreichend gut unterstützt, um die in diesem Kapitel angegebenen Beispiele ausführen zu können. Außerdem unterstützen der Internet Explorer 5 und spätere Versionen die CSS-Merkmale des Level 2 hinreichend gut, um die meisten Beispiele aus Kapitel 18 ausführen zu können. Leider unterstützt der Internet Explorer in keiner der Versionen 5, 5.5 und 6 das Events-Modul des DOM Level 2, obwohl Microsoft an der Definition dieses Moduls mitgewirkt hat und genügend Zeit hatte, um es für den Internet Explorer 6 zu implementieren. Wie Sie in Kapitel 19 sehen werden, ist die Verarbeitung von Events entscheidend für das clientseitige Event-Handling. Durch die mangelnde Unterstützung des Standard-Event-Modells im Internet Explorer wird so die Entwicklung fortgeschrittener Web-Anwendungen behindert.
Obwohl der Internet Explorer 6 (durch seine Methode hasFeature()) angibt, die Kern- und HTML-Interfaces des DOM Level 1 zu unterstützen, ist diese Unterstützung tatsächlich unvollständig. Das ungeheuerlichste Problem - dem Sie sehr wahrscheinlich auch begegnen werden - ist klein, aber sehr ärgerlich: Der Internet Explorer unterstützt die vom Node-Interface definierten Konstanten für Knotentypen nicht. Wie Sie sich erinnern werden, besitzt jeder Knoten eines Dokuments eine Eigenschaft nodeType, die den Typ des Knotens angibt. Die DOM-Spezifikation besagt außerdem, daß das Node-Interface Konstanten definiert, die die einzelnen definierten Knotentypen darstellen. Beispielsweise repräsentiert die Konstante Node.ELEMENT_NODE einen Element-Knoten. Im Internet Explorer (zumindest bis zur Version 6) gibt es diese Konstanten schlichtweg nicht.
Die in diesem Kapitel verwendeten Beispiele sind dahingehend modifiziert worden, daß sie dieses Problem umgehen: Sie verwenden ganzzahlige Werte anstelle der entsprechenden symbolischen Konstanten. Beispielsweise werden Sie Programmzeilen wie die folgende sehen:
if (n.nodeType == 1 /*Node.ELEMENT_NODE*/) // Prüfe, ob n ein Element ist
Die Verwendung von symbolischen Konstanten anstelle von fest codierten ganzzahligen Werten ist guter Programmierstil. Wenn Sie diesen in portabler Weise pflegen wollen, können Sie das folgende Programmstück in Ihre Programme einbinden, mit dem diese Konstanten nötigenfalls definiert werden:
if (!window.Node) {
var Node = { // Wenn es kein Node-Objekt gibt, definiere eines
ELEMENT_NODE: 1, // mit den folgenden Eigenschaften und Werten.
ATTRIBUTE_NODE: 2, // Dies sind nur die HTML-Knotentypen.
TEXT_NODE: 3, // Für XML-spezifische Knoten müssen Sie hier
COMMENT_NODE: 8, // andere Konstanten hinzufügen.
DOCUMENT_NODE: 9,
DOCUMENT_FRAGMENT_NODE: 11
}
}
Obwohl der DOM-Standard aus dem Wunsch heraus entstand, eine gemeinsame API für die Programmierung von dynamischem HTML zu haben, ist das DOM nicht nur für Skript-Programmierer im Web interessant. Tatsächlich wird der Standard derzeit am intensivsten von serverseitigen Java- und C++-Programmen genutzt, die XML-Dokumente analysieren und manipulieren. Aufgrund der vielfältigen Verwendungen ist der DOM-Standard sprachunabhängig definiert. In diesem Buch wird nur die JavaScript-Bindung der DOM-API beschrieben; Sie sollten jedoch über einige andere Punkte Bescheid wissen. Erstens sollten Sie beachten, daß Objekteigenschaften in der JavaScript-Bindung üblicherweise auf Paare von get- und set-Methoden in anderen Sprachen abgebildet werden. Wenn daher ein Java-Programmierer Sie zur Methode getFirstChild() des Node-Interface befragt, müssen Sie wissen, daß die JavaScript-Bindung der Node-API keine Methode getFirstChild() definiert. Statt dessen definiert sie einfach eine Eigenschaft firstChild. Den Wert dieser Eigenschaft in JavaScript zu lesen, entspricht dem Aufruf der Methode getFirstChild() in Java.
Ein weiteres wichtiges Merkmal der JavaScript-Bindung der DOM-API besteht darin, daß sich bestimmte DOM-Objekte wie JavaScript-Arrays verhalten. Wenn ein Interface eine Methode namens item() definiert, verhalten sich Objekte, die dieses Interface implementieren, wie numerische Arrays, die nur gelesen werden können. Nehmen Sie beispielsweise an, Sie hätten ein NodeList-Objekt durch Lesen der Eigenschaft childNodes eines Knotens erhalten. Sie können die einzelnen Node-Objekte in dieser Liste erhalten, indem Sie der Methode item() die gewünschte Knotennummer übergeben oder - einfacher - indem Sie das NodeList-Objekt als Array behandeln und direkt über den Index zugreifen. Das folgende Programmstück verdeutlicht diese beiden Möglichkeiten:
var n = document.documentElement; // Dies ist ein Node-Objekt. var children = n.childNodes; // Dies ist ein NodeList-Objekt. var head = children.item(0); // Eine Möglichkeit, eine NodeList zu benutzen. var body = children[1]; // So ist es einfacher!
Wenn ein DOM-Objekt eine Methode namedItem() besitzt, können Sie wahlweise dieser Methode einen String übergeben oder den String als Array-Index für das Objekt verwenden. Die folgenden drei Programmzeilen sind daher äquivalente Methoden, um auf ein Formularelement zuzugreifen:
var f = document.forms.namedItem("meinFormular");
var g = document.forms["meinFormular"];
var h = document.forms.meinFormular;
Da der DOM-Standard auf verschiedene Weise genutzt werden kann, haben seine Architekten darauf geachtet, daß die DOM-API so definiert wird, daß andere diese API implementieren können, wenn ihnen dies angemessen erscheint. Der DOM-Standard definiert Interfaces anstelle von Klassen. In der objektorientierten Programmierung ist eine Klasse ein fester Datentyp, der genau wie festgelegt implementiert werden muß. Ein Interface ist dagegen eine Sammlung von Methoden und Eigenschaften, die gemeinsam implementiert werden müssen. Daher kann eine Implementation des DOM alle Klassen definieren, die für sie geeignet erscheinen; diese Klassen müssen jedoch die Methoden und Eigenschaften der verschiedenen DOM-Interfaces definieren.
Aus dieser Architektur ergeben sich einige wichtige Konsequenzen. Erstens müssen die in einer Implementation verwendeten Klassennamen nicht unmittelbar den im DOM-Standard (und in diesem Buch) verwendeten Namen der Interfaces entsprechen. Zweitens kann eine einzelne Klasse mehrere Interfaces implementieren. Betrachten Sie beispielsweise das Document-Objekt, das eine Instanz irgendeiner Klasse ist, die von der Implementation des Webbrowsers definiert wird. Wir wissen nicht, wie diese spezielle Klasse heißt, aber wir wissen, daß sie das Interface Document implementiert. Daher stellt uns das Document-Objekt alle Methoden und Eigenschaften zur Verfügung, die von Document definiert werden. Da Webbrowser mit HTML-Dokumenten arbeiten, wissen wir außerdem, daß das Document-Objekt das Interface HTMLDocument implementiert und uns daher auch alle von diesem Interface definierten Methoden und Eigenschaften zur Verfügung stehen. Wenn ein Webbrowser darüber hinaus CSS-Stylesheets unterstützt und das CSS-Modul des DOM implementiert, implementiert das Document-Objekt außerdem die DOM-Interfaces DocumentStyle und DocumentCSS. Unterstützt der Browser dann auch noch die Module Events und Views, implementiert Document außerdem noch die Interfaces DocumentEvent und DocumentView.
Da das DOM in unabhängige Module aufgeteilt ist, definiert es eine Anzahl kleinerer ergänzender Interfaces wie DocumentStyle, DocumentEvent und DocumentView, die jeweils nur eine oder zwei Methoden definieren. Derartige Interfaces werden nie unabhängig vom Kern-Interface implementiert, so daß ich sie nicht unabhängig dokumentiere. Wenn Sie im Referenzteil zum DOM unter dem Interface Document nachschlagen, werden Sie dort auch die Methoden und Eigenschaften der verschiedenen ergänzenden Interfaces aufgeführt finden. Wenn Sie andererseits eines der ergänzenden Interfaces nachschlagen, werden Sie dort lediglich einen Querverweis auf das Kern-Interface finden, mit dem es zusammenhängt. Eine Ausnahme von dieser Regel bilden ergänzende Interfaces, die komplex sind. Beispielsweise wird das Interface HTMLDocument immer von demselben Objekt implementiert, das auch das Document-Objekt implementiert. Da es jedoch wesentliche neue Funktionen hinzufügt, habe ich ihm einen eigenen Referenzeintrag gewidmet.
Eine weitere wichtige Tatsache ist, daß der DOM-Standard keine Konstruktoren definiert, da er Interfaces und keine Klassen definiert. Wenn Sie ein neues Text-Objekt erzeugen und in ein Dokument einfügen möchten, können Sie nicht einfach
var t = new Text("Dies ist ein neuer Textknoten"); // Es gibt keinen solchen
// Konstruktor!
schreiben. Da der DOM-Standard keine Konstruktoren definieren kann, definiert er statt dessen eine Anzahl nützlicher Factory-Methoden zur Erzeugung von Objekten im Document-Interface. Um also einen neuen Text-Knoten für ein Dokument zu erzeugen, würden Sie folgendes schreiben:
var t = document.createTextNode("Dies ist ein neuer Textknoten");
Die Namen der vom DOM definierten Factory-Methoden beginnen mit dem Wort »create«. Zusätzlich zu den von Document definierten Factory-Methoden werden einige weitere von DOMImplementation definiert; sie sind als document.implementation verfügbar.
Nachdem wir die Baumstruktur von Dokumenten betrachtet und uns angesehen haben, wie der Baum aus Node-Objekten aufgebaut ist, können wir uns näher mit den Node-Objekten und Dokumentenbäumen befassen. Wie bereits gesagt wurde, ist die API des DOM-Kerns nicht fürchterlich komplex. Die folgenden Abschnitte enthalten Beispiele, in denen gezeigt wird, wie Sie die am häufigsten auftretenden Aufgaben lösen können.
Wie Sie bereits gesehen haben, repräsentiert das DOM HTML-Dokumente als Bäume von Node-Objekten. Eine der häufigsten Aufgaben im Zusammenhang mit Baumstrukturen ist das Durchlaufen des Baums, wobei jeder Knoten untersucht wird. Beispiel 17-1 zeigt eine Möglichkeit, dies zu tun. Es handelt sich um eine JavaScript-Funktion, die einen Knoten und seine Kinder rekursiv betrachtet und dabei die Anzahl der HTML-Tags (d. h. der Element-Knoten) zählt, die bei diesem Durchlauf gefunden werden. Beachten Sie die Verwendung der Eigenschaft childNodes eines Knotens. Der Wert dieser Eigenschaft ist ein NodeList-Objekt, das sich (in JavaScript) wie ein Array von Node-Objekten verhält. Die Funktion kann daher alle Kinder eines gegebenen Knotens besuchen, indem die Elemente des Arrays childNodes[] durchlaufen werden. Durch einen rekursiven Aufruf werden nicht nur alle Kinder eines gegebenen Knotens, sondern alle Knoten im Baum besucht. Beachten Sie, daß diese Funktion auch die Verwendung der Eigenschaft nodeType zeigt, mit der der Typ eines Knotens ermittelt werden kann.
<head> <script> // Dieser Funktion wird ein DOM-Node-Objekt übergeben. Sie prüft, ob dieser Knoten ein // HTML-Tag repräsentiert, d.h., ob der Knoten ein Element-Objekt ist. Sie // ruft sich selbst rekursiv für jedes Kind des Knotens auf, um dieses // in der gleichen Weise zu prüfen. Sie gibt die Gesamtzahl der Element-Objekte // zurück, die sie findet. Wenn Sie diese Funktion mit dem Document-Objekt als // Argument aufrufen, durchläuft sie den gesamten DOM-Baum. function zaehleTags(n) { // n ist ein Node-Objekt var anzTags = 0; // Initialisiere den Tag-Zähler if (n.nodeType == 1 /*Node.ELEMENT_NODE*/) // Prüfe, ob n ein Element ist anzTags++; // Wenn ja, erhöhe den Zähler var kinder = n.childNodes; // Hole nun alle Kinder von n, for(var i=0; i < kinder.length; i++) { // und durchlaufe sie anzTags += zaehleTags(kinder[i]); // Besuche jedes rekursiv } return anzTags; // Gib die Gesamtzahl der Tags zurück } </script> </head> <!-- Ein Beispiel, wie die Funktion zaehleTags() verwendet werden kann --> <body onload="alert('Dieses Dokument enthält ' + zaehleTags(document) + ' Tags')"> Dies ist ein <i>Beispiel</i> eines Dokuments. </body>
Beachten Sie außerdem, daß die Funktion zaehleTags() in Beispiel 17-1 durch den Event-Handler onload aufgerufen wird - also erst, wenn das Dokument vollständig geladen worden ist. Sie können einen Dokumentenbaum grundsätzlich erst durchlaufen und bearbeiten, wenn das Dokument vollständig geladen worden ist.
Zusätzlich zu childNodes definiert das Interface Node einige weitere nützliche Eigenschaften. firstChild und lastChild verweisen auf das erste und letzte Kind eines Knotens, nextSibling und previousSibling auf die benachbarten Geschwister eines Knotens. (Zwei Knoten sind Geschwister, wenn sie denselben Elternknoten besitzen.) Diese Eigenschaften stellen eine weitere Möglichkeit bereit, mit der die Kinder eines Knotens durchlaufen werden können; dies wird in Beispiel 17-2 gezeigt. Dort wird die Anzahl der Zeichen in allen Text-Knoten innerhalb des <body> des Dokuments gezählt. Beachten Sie, wie die Funktion countCharacters() die Eigenschaften firstChild und nextSibling verwendet, um die Kinder eines Knotens zu durchlaufen.
<head> <script> // Dieser Funktion wird ein DOM-Node-Objekt übergeben. Sie prüft, ob dieser Knoten // einen String mit Text enthält, d.h., ob der Knoten ein Text-Objekt ist. // In diesem Fall gibt sie die Länge des String zurück; anderenfalls ruft // sie sich selbst für jedes Kind des Knotens rekursiv auf und addiert die // Gesamtlänge des gefundenen Texts. Die Kinder eines Knotens werden mit Hilfe // der Eigenschaften firstChild und nextSibling besucht. Wenn ein Text-Knoten // gefunden wurde, bricht die Rekursion ab, da ein Text-Knoten keine // Kinder besitzen kann. function zaehleZeichen(n) { // n ist ein Node-Objekt if (n.nodeType == 3 /*Node.TEXT_NODE*/) // Prüfe, ob n ein Text-Objekt ist return n.length; // Wenn ja, gib seine Länge zurück // Anderenfalls kann n Kinder besitzen, deren Zeichen wir zählen müssen var anzZeichen = 0; // Speichert die Gesamtzahl der Zeichen der Kinder // Statt der Eigenschaft childNodes besucht diese Schleife die Kinder // von n mit Hilfe von firstChild und nextSibling. for(var m = n.firstChild; m != null; m = m.nextSibling) { anzZeichen += zaehleZeichen(m); // Summiere die Gesamtzahl der Zeichen } return anzZeichen; // Gib die Gesamtzahl der Zeichen zurück } </script> </head> <!-- Der Event-Handler onload zeigt ein Beispiel, wie die Funktion countCharacters() verwendet werden kann. Beachten Sie, daß wir nur die Zeichen im <body> des Dokuments zählen wollen und daher document.body an die Funktion übergeben. --> <body onload="alert('Länge des Dokuments: ' + zaehleZeichen(document.body))"> Dies ist ein Beispieldokument.<p>Wie lang ist es? </body>
Die Fähigkeit, alle Knoten eines Dokumentenbaums zu durchlaufen, ermöglicht es uns, bestimmte Knoten zu finden. Wenn man mit der DOM-API programmiert, benötigt man oft einen bestimmten Knoten im Dokument oder eine Liste von Knoten eines bestimmten Typs. Glücklicherweise stellt die DOM-API Funktionen bereit, die uns dies sehr leicht machen.
In Beispiel 17-2 haben wir auf das <body>-Element eines HTML-Dokuments mit dem JavaScript-Ausdruck document.body zugegriffen. Die Eigenschaft body des Document-Objekts ist eine spezielle nützliche Eigenschaft, mit der Sie vorzugsweise auf das <body>-Tag eines HTML-Dokuments zugreifen sollten. Wenn diese Eigenschaft nicht existierte, könnten wir auf das <body>-Tag auch folgendermaßen zugreifen:
document.getElementsByTagName("body")[0]
Dieser Ausdruck ruft die Methode getElementsByTagName() des Document-Objekts auf und selektiert das erste Element des zurückgegebenen Arrays. Der Aufruf von getElementsByTagName() gibt ein Array aller <body>-Elemente im Dokument zurück. Da HTML-Dokumente nur ein <body>-Element enthalten können, sind wir nur an dem ersten Element des zurückgegebenen Arrays interessiert.6
Sie können getElementsByTagName() verwenden, um eine Liste aller HTML-Elemente eines bestimmten Typs zu erhalten. Um beispielsweise alle Tabellen in einem Dokument zu finden, verwenden Sie die folgende Programmzeile:
var tabellen = document.getElementsByTagName("table");
alert("Dieses Dokument enthält " + tables.length + " Tabellen");
Da bei HTML-Tags die Groß- und Kleinschreibung keine Rolle spielt, können Sie auch die an getElementsByTagName() übergebenen Strings schreiben, wie Sie möchten. Die obigen Zeilen finden daher auch <table>-Tags, die als <TABLE> angegeben wurden. getElementsByTagName() gibt die Elemente in der Reihenfolge zurück, wie sie im Dokument auftreten. Wenn Sie den String »*« an getElementsByTagName() übergeben, gibt diese Methode eine Liste aller Elemente im Dokument in der Reihenfolge ihres Auftretens zurück. (Diese spezielle Verwendung wird vom Internet Explorer in den Versionen 5 und 5.5 nicht unterstützt; siehe statt dessen das Internet Explorer-spezifische Array Document.all[] in der Referenz zu clientseitigem JavaScript.)
Manchmal werden Sie keine Liste von Elementen haben, sondern mit einem bestimmten einzelnen Element eines Dokuments arbeiten wollen. Wenn Ihnen genügend viele Informationen über die Struktur des Dokuments vorliegen, können Sie möglicherweise getElementsByTagName() verwenden. Wenn Sie beispielsweise etwas mit dem vierten Absatz in einem Dokument anstellen möchten, können Sie diese Programmzeile verwenden:
var meinAbsatz = document.getElementsByTagName("p")[3];
Dies ist gewöhnlich allerdings nicht die beste (und auch nicht die effizienteste) Technik, da sie eng mit der Struktur des Dokuments verknüpft ist: Ein neu an den Anfang des Dokuments eingefügter Absatz würde das Programm unbrauchbar machen. Wenn Sie daher bestimmte Elemente eines Dokuments bearbeiten müssen, sollten Sie diesen Elementen besser ein id-Attribut zuweisen, das einen (innerhalb des Dokuments) eindeutigen Namen für das Element festlegt. Sie können das gesuchte Element dann über diesen Bezeichner, seine ID, ansprechen. Beispielsweise könnten Sie den besonderen vierten Absatz Ihres Dokuments mit dem folgenden Tag benennen:
<p id="speziellerAbsatz">
Dann können Sie den Knoten dieses Absatzes mit der JavaScript-Zeile ansprechen:
var meinAbsatz = document.getElementById("speziellerAbsatz");
Beachten Sie, daß die Methode getElementById() nicht wie getElementsByTagName() ein Array von Elementen zurückgibt. Da der Wert jedes id-Attributs als eindeutig angenommen wird, gibt getElementById() nur das eine Element mit dem passenden id-Attribut zurück. getElementById() ist eine wichtige Methode, die recht häufig in der DOM-Programmierung verwendet wird.
Sowohl getElementById() als auch getElementsByTagName() sind Methoden des Document-Objekts. Element-Objekte definieren allerdings ebenfalls eine Methode getEle-mentsByTagName(), die sich genau wie die Methode des Document-Objekts verhält, jedoch nur Elemente zurückgibt, die Nachfahren des Elements sind, für das die Methode aufgerufen wurde. Anstatt das gesamte Dokument nach Elementen eines bestimmten Typs zu durchsuchen, durchsucht sie nur das angegebene Element. Dadurch können Sie beispielsweise getElementById() verwenden, um ein bestimmtes Element zu finden. Anschließend können Sie mit getElementsByTagName() alle Nachfahren eines gegebenen Typs innerhalb dieses bestimmten Tags suchen - beispielsweise:
// Finde ein bestimmtes Table-Element in einem Dokument, und zähle seine Zeilen
var inhaltsverzeichnis = document.getElementById("Inhalt");
var zeilen = inhaltsverzeichnis.getElementsByTagName("tr");
var anzZeilen = zeilen.length;
Beachten Sie schließlich noch, daß für HTML-Dokumente das HTMLDocument-Objekt auch eine Methode getElementsByName() definiert. Diese ist getElementById() sehr ähnlich, sucht aber nach dem name-Attribut von Elementen statt nach dem id-Attribut. Da das name-Attribut in einem Dokument nicht eindeutig sein muß (beispielsweise besitzen Radio-Buttons in einem HTML-Formular üblicherweise denselben Wert für name), gibt getElementsByName() ein Array von Elementen anstelle eines einzelnen Elements zurück. Beispielsweise:
// Finde <a name="top">
var link = document.getElementsByName("top")[0];
// Finde alle <input type="radio" name="versandart">-Elemente
var auswahl = document.getElementsByName("versandart");
Die Knoten eines Dokuments zu durchlaufen kann nützlich sein, die eigentliche Kraft der Kern-DOM-API liegt jedoch in den Möglichkeiten, mit JavaScript dynamisch Dokumente zu verändern. Die folgenden Beispiele demonstrieren die grundlegenden Techniken, wie Dokumente modifiziert werden können, und zeigen einige der bestehenden Möglichkeiten auf.
Beispiel 17-3 enthält eine JavaScript-Funktion namens reverse(), ein Beispieldokument und einen HTML-Button. Wenn er angeklickt wird, ruft er die Funktion reverse() auf und übergibt ihr den Knoten, der das <body>-Element des Dokuments repräsentiert. (Beachten Sie die Verwendung von getElementsByTagName() in dem Event-Handler des Buttons, mit dem das <body>-Element gefunden wird.) Die Funktion reverse() durchläuft die Kinder des angegebenen Knotens rückwärts und verwendet die Methoden removeChild() und appendChild() des Node-Objekts, um die Reihenfolge der Kinder umzukehren.
<head><title>Umkehr</title> <script> function reverse(n) { // Kehre die Reihenfolge der Kinder des Knotens Node um var kinder = n.childNodes; // Beschaffe die Liste aller Kinder var anzKinder = kinder.length; // Wie viele Kinder gibt es? for(var i = anzKinder-1; i >= 0; i--) { // Durchlaufe die Kinder rückwärts var c = n.removeChild(kinder[i]); // Entferne ein Kind, n.appendChild(c); // und füge es an neuer Stelle wieder ein } } </script> </head> <body> <p>paragraph #1<p>paragraph #2<p>paragraph #3 <!-- Ein Beispieldokument --> <p> <!-- Ein Button zum Aufruf von reverse()--> <button onclick="reverse(document.body);" >Drück mich, um das Dokument umzukehren.</button> </body>
Das Ergebnis von Beispiel 17-3 sehen Sie in Abbildung 17-3: Wenn der Benutzer auf den Button klickt, wird die Reihenfolge der Absätze und des Buttons umgekehrt.
Einige Dinge an Beispiel 17-3 sind bemerkenswert. Wenn Sie erstens einen Knoten, der bereits in dem Dokument enthalten ist, an appendChild() übergeben, wird dieser zunächst entfernt. Daher hätten wir unser Programm vereinfachen können, indem wir auf den Aufruf von removeChild() verzichten. Zweitens sollten Sie daran denken, daß die Eigenschaft childNodes (wie alle NodeList-Objekte) »lebt«: Wenn das Dokument verändert wird, sind die Änderungen sofort über die NodeList sichtbar. Dies ist ein wichtiges Merkmal des Interface NodeList, allerdings kann es die Programmierung in einigen Fällen etwas komplizierter machen. Beispielsweise ändert ein Aufruf von removeChild() den Index aller auf das Kind folgenden Geschwister, so daß Sie Ihre Schleifen sorgfältig programmieren müssen, wenn Sie eine NodeList durchlaufen und einige der Knoten löschen.
Beispiel 17-4 zeigt eine Variante der Funktion reverse() aus dem vorhergehenden Beispiel. Hier wird eine Rekursion verwendet, mit der nicht nur die Kinder eines angegebenen Knotens, sondern alle Nachfahren des Knotens umgekehrt werden. Wenn die Funktion einen Text-Knoten findet, kehrt sie außerdem auch die Reihenfolge der Zeichen innerhalb dieses Knotens um. Beispiel 17-4 zeigt nur den JavaScript-Code für diese neue reverse()-Funktion. Sie kann einfach in einem HTML-Dokument wie dem aus Beispiel 17-3 verwendet werden.
// Kehre alle Knoten unterhalb des Knotens n um - auch den Inhalt von Text-Knoten function reverse(n) { if (n.nodeType == 3 /*Node.TEXT_NODE*/) { // Kehre Text-Knoten um var text = n.data; // Beschaffe den Inhalt des Knotens var umgekehrt = ""; for(var i = text.length-1; i >= 0; i--) // Drehe ihn um umgekehrt += text.charAt(i); n.data = umgekehrt; // Speichere den umgekehrten Text } else { // Bei anderen Knoten wird rekursiv die Reihenfolge der Kinder umgekehrt var kinder = n.childNodes; var anzKinder = kinder.length; for(var i = anzKinder-1; i >= 0; i--) { // Durchlaufe die Kinder reverse(kinder[i]); // Rekursiver Aufruf für das Kind n.appendChild(n.removeChild(kinder[i])); // Bewege das Kind an die neue Stelle } } }
Beispiel 17-4 zeigt eine Möglichkeit, wie der in einem Dokument angezeigte Text geändert werden kann: Sie weisen einfach dem Feld data des entsprechenden Text-Knotens einen Wert zu. Beispiel 17-5 zeigt eine weitere Möglichkeit. Dieses Beispiel definiert eine Funktion uppercase(), die rekursiv die Kinder eines gegebenen Knotens durchläuft. Wenn sie einen Text-Knoten findet, ersetzt die Funktion diesen Knoten durch einen neuen Text-Knoten, der den Text des ursprünglichen Knotens in Großbuchstaben enthält. Beachten Sie die Verwendung der Methode document.createTextNode(), mit der die neuen Text-Knoten erzeugt werden, und die Verwendung der Methode replaceChild() des Node-Objekts, mit der der ursprüngliche Text-Knoten durch den neu erzeugten ersetzt wird. Beachten Sie außerdem, daß replaceChild() für den Elternknoten des zu ersetzenden Knotens aufgerufen wird, nicht für den Knoten selbst. Die Funktion uppercase() verwendet die Eigenschaft parentNode des Node-Objekts, um den Elternknoten des zu ersetzenden Text-Knotens zu ermitteln.
Neben der Definition der Funktion uppercase() enthält Beispiel 17-5 zwei HTML-Absätze und einen Button. Wenn der Benutzer diesen Button anklickt, wird einer der beiden Absätze in Großbuchstaben umgewandelt. Jeder Absatz wird durch einen eindeutigen Namen identifiziert, der mit dem id-Attribut des <p>-Tags angegeben wird. Der Event-Handler dieses Buttons verwendet die Methode getElementById(), um das Element-Objekt zu beschaffen, das den gewünschten Absatz darstellt.
<script> // Diese Funktion besucht rekursiv das Node-Objekt n und seine Nachfolger, // wobei alle Text-Knoten in Großbuchstaben umgewandelt werden. function uppercase(n) { if (n.nodeType == 3 /*Node.TEXT_NODE*/) { // Wenn der Knoten ein Text-Knoten ist, erzeuge einen neuen Text-Knoten, // der die groß geschriebene Version des Knotentexts enthält, und verwende // die Methode replaceChild() des Elternknotens, um den ursprünglichen // Knoten durch den neuen, groß geschriebenen Knoten zu ersetzen. var neuerKnoten = document.createTextNode(n.data.toUpperCase()); var vater = n.parentNode; vater.replaceChild(neuerKnoten, n); } else { // Wenn der Knoten kein Text-Knoten ist, durchlaufe alle seine Kinder, // und rufe diese Funktion rekursiv für jedes Kind auf. var kinder = n.childNodes; for(var i = 0; i < kinder.length; i++) uppercase(kinder[i]); } } </script> <!-- Hier ist ein wenig Beispieltext. Die <p>-Tags tragen id-Attribute! --> <p id="p1">Dies <i>ist</i> Absatz 1.</p> <p id="p2">Dies <i>ist</i> Absatz 2.</p> <!-- Hier ist ein Button, der die oben definierte Funktion uppercase() aufruft. --> <!-- Durch Aufruf von document.getElementById() finden wir den gewünschten Knoten. --> <button onclick="uppercase(document.getElementById('p1'));">Drück mich</button>
Die beiden vorangegangenen Beispiele zeigen, wie Dokumentinhalt modifiziert werden kann, indem der in einem Text-Knoten enthaltene Text ersetzt wird bzw. ein ganzer Text-Knoten durch einen neuen Text-Knoten ersetzt wird. Sie können Text in einem Text-Knoten auch mit den Methoden appendData(), insertData(), deleteData() und replaceData() anhängen, einfügen, löschen bzw. ersetzen. Diese Methoden werden nicht vom Text-Interface selbst definiert, sondern von CharacterData geerbt. Weitere Informationen zu diesen Methoden finden Sie unter »CharacterData« im DOM-Referenzteil.
In den Beispielen zum Umkehren von Knoten haben Sie gesehen, wie Sie die Methoden removeChild() und appendChild() verwenden können, um die Kinder eines Node-Objekts umzusortieren. Sie können aber nicht nur die Reihenfolge von Knoten innerhalb ihres Elternknotens verändern; die DOM-API ermöglicht das freie Verschieben von Knoten im gesamten Dokumentenbaum (allerdings nur innerhalb eines Dokuments). Beispiel 17-6 demonstriert dies, indem eine Funktion namens fettDarstellen() definiert wird, die einen angegebenen Knoten durch ein neues Element ersetzt (das mit der Methode createElement() des Document-Objekts erzeugt wird). Dieses neue Element repräsentiert ein HTML-Tag <b> und weist dem ursprünglichen Knoten diesen neuen <b>-Knoten als Elternknoten zu. In einem HTML-Dokument wird dadurch der in dem Knoten und seinen Nachfolgern enthaltene Text in fetter Schrift dargestellt.
<script> // Dieser Funktion wird ein Node-Objekt n übergeben. Es wird im Baum durch einen // Element-Knoten ersetzt, der ein HTML-Tag <b> darstellt, und weist den ursprünglichen // Knoten dem neuen <b>-Element als Kind zu. function fettDarstellen(node) { var fett = document.createElement("b"); // Erzeuge ein neues <b>-Element var vater = node.parentNode; // Beschaffe den Elternknoten vater.replaceChild(fett, node); // Ersetze den Knoten durch das <b>-Tag fett.appendChild(node); // Ordne den Knoten dem <b>-Tag als Kind zu } </script> <!-- Einige Beispielabsätze --> <p id="p1">Dies <i>ist</i> Absatz #1.</p> <p id="p2">Dies <i>ist</i> Absatz #2.</p> <!-- Ein Button, der die Funktion fettDarstellen() für den ersten Absatz aufruft --> <button onclick="fettDarstellen(document.getElementById('p1'));">Bitte in fett</button>
Dokumente können nicht nur durch das Einfügen, Löschen und Umgruppieren von Knoten verändert werden. Umfassende Änderungen eines Dokuments können auch einfach durch die Zuweisung von Attributwerten an Dokumentelemente erfolgen. Dies geht unter anderem mit Hilfe der Methode element.setAttribute(). Beispielsweise:
// Finde ein benanntes Element
var ueberschrift = document.getElementById("ueberschrift");
ueberschrift.setAttribute("align", "center"); // Setze align='center'
Die DOM-Elemente, die HTML-Attribute darstellen, definieren JavaScript-Eigenschaften, die den einzelnen Standardattributen entsprechen (selbst für veraltete Attribute wie align), so daß Sie das gleiche Resultat mit den folgenden Programmzeilen erzielen können:
var ueberschrift = document.getElementById("ueberschrift");
ueberschrift.align = "center"; // Setzen des alignment-Attributs.
Die vorangegangenen beiden Beispiele haben gezeigt, wie der Inhalt eines Text-Knotens in Großbuchstaben umgewandelt und wie ein Knoten als Kind einem neu erzeugten <b>-Knoten zugeordnet werden kann. Die Funktion fettDarstellen() hat gezeigt, daß es möglich ist, neue Knoten zu erzeugen und zu einem Dokument hinzuzufügen. Sie können einem Dokument beliebigen Inhalt hinzufügen, indem Sie die benötigten Element- und Text-Knoten mit document.createElement() und document.createTextNode() erzeugen und in geeigneter Weise zu dem Dokument hinzufügen. Dies wird in Beispiel 17-7 demonstriert, das eine Funktion namens debug() definiert. Diese Funktion ermöglicht es in bequemer Weise, Debugging-Meldungen in ein Programm einzufügen. Sie dient als eine nützliche Alternative zur eingebauten Funktion alert(). Ein Beispiel für die Verwendung dieser Funktion debug() ist in Abbildung 17-4 zu sehen.
Wenn debug() erstmals aufgerufen wird, verwendet die Funktion die DOM-API, um ein <div>-Element zu erzeugen und an das Ende des Dokuments anzuhängen. Die bei diesem ersten und allen weiteren Aufrufen an debug() übergebenen Debugging-Meldungen werden dann in dieses <div>-Element eingefügt. Jede Debugging-Meldung wird angezeigt, indem ein Text-Knoten in einem <p>-Element erzeugt und dieses an das Ende des <div>-Elements angehängt wird.
Beispiel 17-7 demonstriert auch eine bequeme, allerdings nicht standardisierte Art, wie neuer Inhalt zu einem Dokument hinzugefügt werden kann. Das <div>-Element, das die Debugging-Meldungen enthält, zeigt zentriert in großer Schrift einen Titel an. Dieser Titel könnte in der gleichen Weise erzeugt und zu dem Dokument hinzugefügt werden wie anderer Inhalt auch. In diesem Beispiel nutzen wir statt dessen die Eigenschaft innerHTML des <div>-Elements. Wenn dieser Eigenschaft eines Elements ein String mit HTML-Text zugewiesen wird, wird dieser String analysiert und in das Element als Inhalt eingefügt. Obwohl diese Eigenschaft nicht Teil der DOM-API ist, stellt sie eine nützliche Abkürzung dar, die vom Internet Explorer 4 und neueren Versionen sowie vom Netscape 6 unterstützt wird. Obwohl sie nicht dem Standard entspricht, wird sie häufig benutzt und wird in diesem Beispiel der Vollständigkeit halber gezeigt.7
/** * Diese Funktion debug zeigt aus einfachem Text bestehende Debugging- * Meldungen in einem separaten Kasten am Ende eines Dokuments an. Sie ist eine * nützliche Alternative zur Verwendung von alert(), um solche Meldungen anzuzeigen. **/ function debug(msg) { // Wenn wir noch keinen Kasten erzeugt haben, in dem unsere Debugging- // Meldungen angezeigt werden sollen, machen wir dies nun. Um die Verwendung // einer weiteren globalen Variablen zu vermeiden, speichern wir den Knoten // des Kastens als Eigenschaft dieser Funktion. if (!debug.box) { // Erzeuge ein neues <div>-Element debug.box = document.createElement("div"); // Das Aussehen wird mit CSS-Style-Attributen festgelegt debug.box.setAttribute("style", "background-color: white; " + "font-family: monospace; " + "border: solid black 3px; " + "padding: 10px;"); // Das neue <div>-Element wird an das Ende des Dokuments angehängt document.body.appendChild(debug.box); // Füge dem <div> einen Titel hinzu. Wir verwenden die Eigenschaft innerHTML, // um das HTML-Fragment zu analysieren und in das Dokument einzufügen. // innerHTML ist nicht Teil des W3C-DOM-Standards, wird aber vom // Netscape 6 und Internet Explorer 4 und neueren Versionen unterstützt. Wir können // die Verwendung von innerHTML vermeiden, indem wir ausdrücklich ein // <h1>-Element erzeugen, dessen Stilattribut setzen, ihm einen Text-Knoten // hinzufügen und das Ganze dann in das Dokument einfügen. // Die Eigenschaft innerHTML ist hier eine nette Abkürzung. debug.box.innerHTML = "<h1 style='text-align:center'>Debugging-Meldungen</h1>"; } // Wenn wir hier ankommen, verweist debug.box auf ein <div>-Element, // in das wir unsere Debugging-Meldungen einfügen können. // Erzeuge zunächst einen <p>-Knoten für die Meldung. var p = document.createElement("p"); // Erzeuge dann einen Text-Knoten mit der Nachricht, und füge ihn dem <p> hinzu p.appendChild(document.createTextNode(msg)); // Hänge den <p>-Knoten an das <div> an, das die Meldungen enthält. debug.box.appendChild(p); }
Die in Beispiel 17-7 gezeigte debug()-Methode kann in HTML-Dokumenten wie dem folgenden verwendet werden (die Ausgabe dazu sehen Sie in Abbildung 17-4):
<script src="Debug.js"></script> <!-- Binde die Funktion debug() ein -->
<script>var anzahl=0;</script> <!-- Definiere eine globale Variable -->
<!-- Verwende nun die debug()-Funktion in einem Event-Handler -->
<button onclick="debug('clicked: ' + anzahl++);">Drück mich</button>
Die API des DOM-Kerns definiert das DocumentFragment-Objekt, mit dem man Gruppen von Document-Knoten bearbeiten kann. Ein DocumentFragment-Objekt ist ein besonderer Typ von Knoten, der nicht im Dokument selbst auftaucht, aber als temporärer Behälter für eine Sammlung von aufeinanderfolgenden Knoten dient. Diese Knoten können dann wie ein einzelner Knoten behandelt werden. Wenn ein DocumentFragment-Objekt in ein Dokument eingefügt wird (mit einer der Methoden appendChild(), insertBefore() oder replaceChild() des Node-Objekts), wird nicht das DocumentFragment-Objekt selbst, sondern jedes seiner Kinder eingefügt.
Beispielsweise können Sie ein DocumentFragment-Objekt verwenden, um die Methode reverse() aus Beispiel 17-3 folgendermaßen umzuschreiben:
function reverse(n) { // Kehre die Reihenfolge der Kinder des Knotens Node um
var f = document.createDocumentFragment(); // Beschaffe ein leeres DocumentFragment
while(n.lastChild) // Durchlaufe die Kinder rückwärts
f.appendChild(n.lastChild); // und verschiebe sie in das DocumentFragment
n.appendChild(f); // Verschiebe sie dann zurück (in der neuen
// Reihenfolge)
}
Nachdem Sie ein DocumentFragment-Objekt erzeugt haben, können Sie es in der folgenden Weise verwenden:
document.getElementsByTagName("p")[0].appendChild(fragment);
Wenn Sie ein DocumentFragment-Objekt in ein Dokument einfügen, werden die Kinder des Fragments von diesem in das Dokument verschoben. Nach dem Einfügen ist das Fragment leer und kann erst wieder verwendet werden, nachdem Sie ihm neue Kinder hinzugefügt haben. Das DocumentFragment-Objekt wird uns weiter unten in diesem Kapitel wiederbegegnen, wenn wir die DOM-Range-API betrachten.
Die vorangegangenen Abschnitte haben gezeigt, wie Sie die API des DOM-Kerns verwenden können, um Inhalte eines Dokuments zu durchlaufen und zu verändern sowie um einem Dokument Inhalte hinzuzufügen. Beispiel 17-8 am Ende dieses Abschnitts setzt alle diese Teile zu einem größeren Beispiel zusammen. In dem Beispiel wird eine einzige Methode maketoc() definiert, die einen Document-Knoten als einziges Argument entgegennimmt. maketoc() durchläuft das Dokument, erzeugt ein Inhaltsverzeichnis (englisch: »Table of Contents«, kurz »TOC«) und ersetzt den angegebenen Knoten durch das neu erzeugte Verzeichnis. Das Inhaltsverzeichnis wird erzeugt, indem die Tags <h1>, <h2>, <h3>, <h4>, <h5> und <h6> in dem Dokument gesucht werden - wobei wir davon ausgehen, daß diese Tags die wichtigen Abschnitte des Dokuments markieren. Zusätzlich zur Erzeugung des Verzeichnisses fügt die Funktion maketoc() benannte Anker (<a>-Elemente mit einem name-Attribut anstelle des href-Attributs) an den Anfang jeder Überschrift ein, so daß das Inhaltsverzeichnis einen Hypertext-Verweis zu jedem Abschnitt bereitstellen kann. Zu guter Letzt fügt maketoc() am Anfang eines jeden Abschnitts einen Verweis auf das Inhaltsverzeichnis ein. Wenn der Leser an den Anfang eines neuen Abschnitts gelangt, kann er entweder diesen Abschnitt lesen oder mit dem Verweis zum Inhaltsverzeichnis springen und dort einen anderen Abschnitt auswählen. Abbildung 17-5 zeigt, wie ein mit der Funktion maketoc() erzeugtes Inhaltsverzeichnis aussieht.
Wenn Sie lange Dokumente warten und überarbeiten müssen, die mit den Tags <h1>, <h2> usw. in Abschnitte untergliedert sind, kann die Funktion maketoc() für Sie interessant sein. Inhaltsverzeichnisse sind in längeren Dokumenten sehr nützlich; wenn das Dokument jedoch häufig überarbeitet wird, kann es schwierig sein, das Inhaltsverzeichnis auf dem aktuellen Stand zu halten. Das Inhaltsverzeichnis dieses Buchs wurde automatisch aus dem Inhalt des Buchs erzeugt. maketoc() ermöglicht Ihnen etwas Ähnliches für Ihre Web-Dokumente. Sie können die Funktion in einem HTML-Dokument folgendermaßen benutzen:
<script src="TOC.js"></script> <!-- Lade die Funktion maketoc() -->
<!-- Rufe die Funktion maketoc() auf, wenn das Dokument vollständig geladen ist -->
<body onload="maketoc(document.getElementById('platzhalter'))">
<!-- Dieses span-Element wird durch das erzeugte Verzeichnis ersetzt -->
<span id="platzhalter">Inhaltsverzeichnis</span>
// ... Hier steht der Rest des Dokuments ...
Sie können das Inhaltsverzeichnis mit der Funktion maketoc() auch erst in dem Moment erzeugen, wenn der Benutzer es verwenden möchte. Dazu können Sie einen Hypertext-Verweis (oder einen Button) in das Dokument aufnehmen, der sich selbst durch das Inhaltsverzeichnis ersetzt, sobald der Benutzer ihn anklickt:
<a href="#" onclick="maketoc(this); return false;">Inhaltsverzeichnis anzeigen</a>
Im Folgenden finden Sie den Code zur Funktion maketoc(). Beispiel 17-8 ist zwar lang, aber ausführlich kommentiert. Die verwendeten Techniken haben wir bereits vorab vorgestellt. Es lohnt sich, dieses aus der Praxis stammende Beispiel ausgiebig zu studieren, da es die Möglichkeiten der DOM-API gut demonstriert. Beachten Sie, daß die Funktion maketoc() zwei Hilfsfunktionen benötigt, die aus Gründen der Modularität innerhalb von maketoc() definiert sind. Auf diese Weise müssen wir dem globalen Namensraum keine anderweitig unnützen Funktionen hinzufügen.
/** * Erzeuge ein Inhaltsverzeichnis für dieses Dokument, und füge es in das * Dokument ein, indem der durch das Argument angegebene Knoten ersetzt wird. **/ function maketoc(ersetze) { // Erzeuge ein <div>-Element, das die Wurzel des Inhaltsverzeichnisses bildet var toc = document.createElement("div"); // Setze eine Hintergrundfarbe und eine Schriftart für das Verzeichnis // Im nächsten Kapitel erfahren Sie mehr über die style-Eigenschaft. toc.style.backgroundColor = "white"; toc.style.fontFamily = "sans-serif"; // Beginne das Verzeichnis mit einem Anker, auf den wir zurückverweisen können var anker = document.createElement("a"); // Erzeuge einen <a>-Knoten anker.setAttribute("name", "TOC"); // Benenne ihn toc.appendChild(anker); // Füge ihn ein // In dem Anker steht der Titel des Inhaltsverzeichnisses anker.appendChild(document.createTextNode("Inhaltsverzeichnis")); // Erzeuge ein <table>-Element, das das Verzeichnis enthält; füge es hinzu var tabelle = document.createElement("table"); toc.appendChild(tabelle); // Erzeuge ein <tbody>-Element, das die Zeilen des Verzeichnisses enthält var tabellenrumpf = document.createElement("tbody"); tabelle.appendChild(tabellenrumpf); // Initialisiere ein Array, das die Abschnitte mitzählt var abschnittsnummern = [0,0,0,0,0,0]; // Durchlaufe den Body des Dokuments rekursiv, und suche nach Abschnitten, // die mit den Tags <h1>, <h2>, ... markiert sind. Erstelle aus ihnen // das Inhaltsverzeichnis durch Hinzufügen von Zeilen zur Tabelle abschnitteHinzufuegen(document.body, tabellenrumpf, abschnittsnummern); // Füge schließlich das Verzeichnis in das Dokument ein, indem der durch // das Argument ersetze angegebene Knoten durch den Verzeichnisbaum ersetzt wird ersetze.parentNode.replaceChild(toc, ersetze); // Diese Methode durchläuft rekursiv den Baum unter dem Node-Objekt n, // sucht dort nach den Tags <h1> bis <h6> und verwendet deren Inhalt, um das // Inhaltsverzeichnis aufzubauen. Dazu werden der als Argument toc übergebenen // HTML-Tabelle Zeilen hinzugefügt. Das Array sectionNumbers wird verwendet, // um die Nummer des aktuellen Abschnitts mitzuzählen. // Diese Funktion ist innerhalb von maketoc() definiert, so daß sie von // außen nicht sichtbar ist. Nur die Funktion maketoc() wird von diesem // JavaScript-Modul exportiert. function abschnitteHinzufuegen(n, toc, abschnittsnummern) { // Durchlaufe alle Kinder von n for(var m = n.firstChild; m != null; m = m.nextSibling) { // Prüfe, ob m ein Überschriftselement ist. Es wäre schön, wenn wir // einfach (m instanceof HTMLHeadingElement) verwenden könnten, doch wird // dies nicht vom Standard verlangt; im IE würde dies nicht funktionieren. // Daher müssen wir schauen, ob der Name des Tags H1,... , H6 ist. if ((m.nodeType == 1) && /* Node.ELEMENT_NODE */ (m.tagName.length == 2) && (m.tagName.charAt(0) == "H")) { // Ermittle die Gliederungstiefe des aktuellen Abschnitts var tiefe = parseInt(m.tagName.charAt(1)); if (!isNaN(tiefe) && (tiefe >= 1) && (tiefe <= 6)) { // Erhöhe die Abschnittsnummer für diese Gliederungsebene abschnittsnummern[tiefe-1]++; // Und setze alle niedrigeren Ebenen auf null zurück for(var i = tiefe; i < 6; i++) abschnittsnummern[i] = 0; // Bastele nun aus den Abschnittsnummern aller Ebenen // eine Nummer wie "2.3.1" für den aktuellen Abschnitt var abschnittsnummer = ""; for(var i = 0; i < tiefe; i++) { abschnittsnummer += abschnittsnummern[i]; if (i < tiefe-1) abschnittsnummer += "."; } // Erzeuge einen Anker, um den Anfang dieses Abschnitts zu markieren. // Er dient als Ziel für den Link im Inhaltsverzeichnis var anker = document.createElement("a"); anker.setAttribute("name", "SECT"+abschnittsnummer); // Erzeuge einen Link zurück zum Inhaltsverzeichnis, // und trage ihn als Kind des Ankers ein var zumInhalt = document.createElement("a"); zumInhalt.setAttribute("href", "#TOC"); zumInhalt.appendChild(document.createTextNode("Inhalt")); anker.appendChild(zumInhalt); // Füge den Anker in das Dokument direkt vor der // Überschrift des Abschnitts ein n.insertBefore(anker, m); // Erzeuge nun einen Verweis zu diesem Abschnitt. Er wird // weiter unten zum Inhaltsverzeichnis hinzugefügt. var verweis = document.createElement("a"); verweis.setAttribute("href", "#SECT" + abschnittsnummer); // Lies den Text der Überschrift mit einer Funktion, die wir unten // definieren var ueberschrift = liesText(m); // Verwende den Text der Überschrift als Inhalt des Verweises verweis.appendChild(document.createTextNode(ueberschrift)); // Erzeuge eine neue Zeile im Verzeichnis var zeile = document.createElement("tr"); // Erzeuge zwei Spalten in der Zeile var spalte1 = document.createElement("td"); var spalte2 = document.createElement("td"); // Die erste Spalte wird rechtsbündig ausgerichtet // und enthält die Nummer des Abschnitts spalte1.setAttribute("align", "right"); spalte1.appendChild(document.createTextNode(abschnittsnummer)); // Die zweite Spalte enthält den Verweis zum Abschnitt spalte2.appendChild(verweis); // Füge beide Spalten der Zeile und diese Zeile der Tabelle hinzu zeile.appendChild(spalte1); zeile.appendChild(spalte2); toc.appendChild(zeile); // Modifiziere das Überschriftselement des Abschnitts, so // daß die Nummer des Abschnitts Teil des Titels wird m.insertBefore(document.createTextNode(abschnittsnummer+": "), m.firstChild); } } else { // Wenn es kein Überschriftselement ist, gehen wir in die Rekursion abschnitteHinzufuegen(m, toc, abschnittsnummern); } } } // Diese Hilfsfunktion durchläuft das Node-Objekt n und gibt den Inhalt // aller gefundenen Text-Knoten ohne HTML-Tags zurück. Auch diese Funktion // ist innerhalb von maketoc() definiert und daher nur in diesem Modul sichtbar. function liesText(n) { var s = ''; var kinder = n.childNodes; for(var i = 0; i < kinder.length; i++) { var kind = kinder[i]; if (kind.nodeType == 3 /*Node.TEXT_NODE*/) s += kind.data; else s += getTextContent(kind); } return s; } }
Webbrowser zeigen HTML-Dokumente an; allerdings gewinnen XML-Dokumente als Datenquellen zunehmend an Bedeutung. Da das DOM es uns ermöglicht, sowohl HTML- als auch XML-Dokumente zu durchlaufen und zu verändern, können wir Methoden des DOM verwenden, um ein XML-Dokument zu laden, Informationen aus ihm zu extrahieren und dynamisch eine HTML-Version dieser Informationen zu erzeugen, die in einem Webbrowser angezeigt werden kann. Beispiel 17-9 zeigt, wie dies im Netscape 6.1 und im Internet Explorer 6 funktioniert. Das Beispiel besteht aus einer HTML-Datei, die hauptsächlich JavaScript-Code enthält. Die Datei muß mit einer URL geladen werden, die den Abfrageteil der URL dazu verwendet, die URL relativ anzugeben, aus der die Daten geladen werden sollen. Beispielsweise können Sie die Beispieldatei mit einer URL wie der folgenden aufrufen:
file://C:/javascript/ZeigeMitarbeiterdaten.html?daten.xml
ZeigeMitarbeiterdaten.html ist der Name der Beispieldatei, daten.xml der Name der zu lesenden XML-Datei. Die XML-Datei muß Daten enthalten, die in der folgenden Weise formatiert sind:
<mitarbeiterliste> <mitarbeiter name="H. Kunz"><job>Programmierer</job><gehalt>32768</gehalt></mitarbeiter> <mitarbeiter name="O. Metzger"><job>Vertrieb</job><gehalt>70000</gehalt></mitarbeiter> <mitarbeiter name="W. Wichtig"><job>CEO</job><gehalt>1000000</gehalt></mitarbeiter> </mitarbeiterliste>
Das Beispiel enthält zwei JavaScript-Funktionen. Die erste, ladeXML(), ist eine generische Funktion, mit der eine beliebige XML-Datei geladen werden kann. Sie enthält Code mit Standardanweisungen des DOM Level 2, um das XML-Dokument zu laden, sowie Code der proprietären Microsoft-API zu demselben Zweck. Das einzig Neue an diesem Beispiel ist die Erzeugung eines neuen Document-Objekts mit der Methode DOMImplementation. createDocument() sowie der Aufruf der Methode load() dieses Document-Objekts. Beachten Sie, daß Dokumente nicht sofort geladen werden; daher ist der Aufruf von ladeXML() beendet, bevor das Dokument geladen wurde. Aus diesem Grund übergeben wir ladeXML() eine Referenz auf eine andere Funktion, die sie aufrufen soll, wenn das Dokument vollständig geladen worden ist.
Die andere in dem Beispiel enthaltene Funktion ist baueTabelle(). Diese Funktion übergeben wir an ladeXML(). Wenn die XML-Datei vollständig geladen worden ist, wird das die XML-Datei darstellende Document-Objekt zusammen mit der URL der Datei an baueTabelle() übergeben. baueTabelle() verwendet Methoden des DOM, die wir bereits kennengelernt haben: Mit ihnen extrahieren wir Informationen aus dem XML-Dokument und fügen sie in eine Tabelle in dem HTML-Dokument ein, das dann letztendlich im Browser angezeigt wird. Diese Funktion demonstriert auch die Verwendung einiger nützlicher von den Interfaces HTMLTableElement, HTMLTableRowElement und verwandten Interfaces definierter Methoden, mit denen Tabellen bearbeitet werden können. Genauere Informationen zu diesen speziell für Tabellen verwendbaren Interfaces und Methoden finden Sie im Referenzteil zum DOM. Obwohl die in dieser Funktion verwendeten Methoden und Eigenschaften des DOM alle recht einfach sind, werden sie hier eng miteinander verwoben. Sie sollten sich das Programm gründlich ansehen - und werden feststellen, daß es recht leicht verständlich ist.
<head><title>Mitarbeiterdaten</title> <script> // Diese Funktion lädt das XML-Dokument von der angegebenen URL und übergibt, // wenn es vollständig geladen worden ist, das Dokument und die URL an die angegebene // Funktion zur Bearbeitung. Diese Funktion funktioniert für beliebige XML-Dokumente. function ladeXML(url, bearbeiter) { // Verwende die Standardtechnik des DOM Level 2, falls sie unterstützt wird if (document.implementation && document.implementation.createDocument) { // Erzeuge ein neues Document-Objekt var xmldok = document.implementation.createDocument("", "", null); // Gib an, was passieren soll, wenn das Dokument geladen worden ist xmldok.onload = function() { bearbeiter(xmldok, url); } // Und teile die zu ladende URL mit xmldok.load(url); } // Verwende ansonsten die proprietäre API des Internet Explorer else if (window.ActiveXObject) { var xmldok = new ActiveXObject("Microsoft.XMLDOM"); // Erzeuge ein Dok. xmldok.onreadystatechange = function() { // Gib onload an if (xmldok.readyState == 4) bearbeiter(xmldok, url); } xmldok.load(url); // Und laden! } } // Diese Funktion erstellt eine HTML-Tabelle der Mitarbeiter aus den Daten, die // sie aus dem übergebenen XML-Dokument erhält function baueTabelle(xmldok, url) { // Erzeuge ein <table>-Objekt, und füge es in das Dokument ein var tabelle = document.createElement("table"); tabelle.setAttribute("border", "1"); document.body.appendChild(tabelle); // Verwende Hilfsmethoden von HTMLTableElement und verwandten Interfaces, // um einen Titel für die Tabelle und eine Überschrift für jede Spalte anzugeben var titel = "Mitarbeiterdaten aus der Datei " + url; tabelle.createCaption().appendChild(document.createTextNode(titel)); var kopf = tabelle.createTHead(); var kopfzeile = kopf.insertRow(0); kopfzeile.insertCell(0).appendChild(document.createTextNode("Name")); kopfzeile.insertCell(1).appendChild(document.createTextNode("Job")); kopfzeile.insertCell(2).appendChild(document.createTextNode("Gehalt")); // Suche nun alle <mitarbeiter>-Elemente im Dokument xmldok var mitarbeiterliste = xmldok.getElementsByTagName("mitarbeiter"); // Durchlaufe diese <mitarbeiter>-Elemente for(var i = 0; i < mitarbeiterliste.length; i++) { // Lies für jeden Mitarbeiter die Daten zu Name, Job und Gehalt mit den // Standardmethoden // des DOM. Der Name kommt aus einem Attribut, die anderen Werte // aus Text-Knoten in <job>- und <salary>-Tags. var e = mitarbeiterliste[i]; var name = e.getAttribute("name"); var job = e.getElementsByTagName("job")[0].firstChild.data; var gehalt = e.getElementsByTagName("gehalt")[0].firstChild.data; // Wenn wir die Mitarbeiterdaten gelesen haben, verwende Methoden der Tabelle, // um eine neue Zeile zu erzeugen, und Methoden der Tabellenzeile, um neue // Zellen zu erzeugen, die die Daten als Text-Knoten enthalten. var zeile = tabelle.insertRow(i+1); zeile.insertCell(0).appendChild(document.createTextNode(name)); zeile.insertCell(1).appendChild(document.createTextNode(job)); zeile.insertCell(2).appendChild(document.createTextNode(gehalt)); } } </script> </head> <!-- Der Body des Dokuments enthält keinen statischen Text; hier wird alles dynamisch mit der Funktion makeTable() erzeugt. Der Event-Handler onload eröffnet das Spiel, indem er ladeXML() aufruft, um die XML-Datei zu laden. Beachten Sie, wie mit location.search der Name der XML-Datei im Abfrageteil der URL gesucht wird. Diese HTML-Datei muß mit einer URL wie ZeigeMitarbeiterdaten.html?daten.xml geladen werden. --> <body onload="ladeXML(location.search.substring(1), baueTabelle)"> </body>
Obwohl der Internet Explorer 4 nicht DOM-konform ist, besitzt er Merkmale, die der API des DOM-Kerns ähneln. Diese Merkmale sind nicht Bestandteil des DOM-Standards und sind nicht kompatibel zu Netscape, wohl aber zu neueren Versionen des Internet Explorers. Im Folgenden werden diese Merkmale zusammengefaßt; genauere Einzelheiten finden Sie im clientseitigen Referenzteil dieses Buchs.
Der DOM-Standard legt fest, daß alle Node-Objekte, also insbesondere das Document-Objekt und alle Element-Objekte, ein Array childNodes[] besitzen, das die Kinder dieses Knotens enthält. Der Internet Explorer 4 unterstützt childNodes[] nicht, stellt aber ein Array children[] für seine Document- und HTMLElement-Objekte bereit, das diesem sehr ähnlich ist. Daher ist es sehr einfach, eine rekursive Funktion wie die in Beispiel 17-1 gezeigte zu schreiben, um die HTML-Elemente in einem Dokument im Internet Explorer 4 vollständig zu durchlaufen.
Es gibt jedoch einen wesentlichen Unterschied zwischen dem Array children[] des Internet Explorer 4 und dem Array childNodes[] des DOM-Standards. Der Internet Explorer 4 besitzt keinen Typ für Text-Knoten und betrachtet Strings mit Text nicht als Kinder. Ein <p>-Tag, das lediglich einfachen Text ohne Markup enthält, besitzt im Internet Explorer 4 daher ein leeres Array children[]. Wir werden jedoch weiter unten sehen, daß Sie auf den Text-Inhalt eines <p>-Tags über die Eigenschaft innerText des Internet Explorer 4 zugreifen können.
Der Internet Explorer 4 unterstützt die Methoden getElementById() und getEle-mentsByTagName() des Document-Objekts nicht. Statt dessen besitzen das Document-Objekt und alle Dokumentelemente eine Array-Eigenschaft namens all[]. Wie der Name nahelegt, stellt dieses Array alle Elemente eines Dokuments bzw. alle Elemente innerhalb eines anderen Elements dar. Beachten Sie, daß all[] nicht nur die Kinder des Dokuments bzw. Elements repräsentiert, sondern alle Nachfahren, wie tief auch immer sie verschachtelt sein mögen.
Das Array all[] kann auf verschiedene Weise genutzt werden. Wenn Sie mit einem Index n darauf zugreifen, gibt es das n+1. Element des Dokuments bzw. Eltern-Elements zurück. Beispielsweise:
var e1 = document.all[0]; // Das erste Element des Dokuments var e2 = e1.all[4]; // Das fünfte Element von Element 1
Elemente werden in der Reihenfolge numeriert, wie sie im Quelltext des Dokuments auftreten. Beachten Sie jedoch den einen wesentlichen Unterschied zwischen den APIs des Internet Explorer 4 und des DOM-Standards: Der Internet Explorer kennt keine Text-Knoten, so daß das Array all[] nur Dokumentelemente enthält, aber keinen in diese eingeschlossenen Text.
Meist ist es hilfreicher, wenn Sie auf Dokumentelemente über ihren Namen statt über einen Index zugreifen können. Die Entsprechung zu getElementbyId() besteht im Internet Explorer 4 darin, das Array all[] mit einem String statt einer Zahl aufzurufen. Der Internet Explorer 4 gibt dann das Element zurück, dessen id- oder name-Attribut den angegebenen Wert besitzt. Wenn es mehrere solcher Elemente gibt (was vorkommen kann, da verschiedene Formularelemente, wie beispielsweise Radio-Buttons, oft mit demselben name-Attribut versehen werden), wird ein Array dieser Elemente zurückgegeben. Beispielsweise:
var speziellerAbsatz = document.all["speziell"]; var radioButtons = form.all["versandart"]; // Kann ein Array zurückgeben
In JavaScript können wir diese Ausdrücke auch schreiben, indem wir den Index des Arrays als Namen einer Eigenschaft angeben:
var speziellerAbsatz = document.all.speziell; var radioButtons = form.all.versandart;
Indem wir das Array all[] in dieser Weise verwenden, erhalten wir die Grundfunktionalität von getElementById() und getElementsByName() auch im Internet Explorer 4. Der Hauptunterschied besteht darin, daß das Array all[] die Merkmale dieser beiden Methoden miteinander verbindet. Dies kann zu Problemen führen, wenn Sie versehentlich dieselben Werte für id- und name-Attribute von Elementen verwenden, die nichts mitein-ander zu tun haben.
Das Array all[] besitzt eine ungewöhnliche Eigenheit: eine Methode tags(), mit der Sie ein Array von Elementen anhand des Tag-Namens erhalten können. Beispielsweise:
var listen = document.all.tags("UL"); // Finde alle <ul>-Tags im Dokument
var elemente = lists[0].all.tags("LI"); // Finde alle <li>-Tags im ersten <ul>
Diese Syntax des Internet Explorer 4 stellt im wesentlichen die gleiche Funktionalität bereit wie die Methode getElementsByTagName() von Document- und Element-Objekten im DOM. Beachten Sie, daß der Name des Tags im Internet Explorer 4 in Großbuchstaben angegeben werden sollte.
Wie der DOM-Standard stellt auch der Internet Explorer 4 die Attribute von HTML-Tags als Eigenschaften der entsprechenden HTMLElement-Objekte bereit. Daher können Sie ein im Internet Explorer 4 angezeigtes Dokument modifizieren, indem Sie dynamisch seine HTML-Attribute ändern. Wenn eine Attributänderung die Größe eines Elements betrifft, wird das Dokument neu angelegt, um die Größenänderung zu berücksichtigen. Das HTMLElement-Objekt des Internet Explorer 4 definiert außerdem Methoden setAttribute(), getAttribute() und removeAttribute(), die den gleichnamigen Methoden des Element-Objekts der API des DOM-Standards ähneln.
Der DOM-Standard definiert eine API, die die Erzeugung neuer Knoten, das Einfügen von Knoten in den Dokumentenbaum sowie das Verschieben von Knoten innerhalb des Baums ermöglicht. Der Internet Explorer 4 verfügt über diese Möglichkeiten nicht. Statt dessen definieren jedoch alle HTMLElement-Objekte im Internet Explorer 4 eine Eigenschaft innerHTML. Wenn Sie dieser Eigenschaft einen String mit HTML-Text zuweisen, können Sie den Inhalt eines Elements durch beliebigen neuen Inhalt ersetzen. Da die innerHTML-Eigenschaft so mächtig ist, wurde sie auch im Netscape 6 (und im Mozilla-Browser, auf dessen Grundlage der Netscape 6 entwickelt wurde) implementiert, obwohl sie nicht Bestandteil des DOM-Standards ist. Die Verwendung von innerHTML wurde in Beispiel 17-7 gezeigt.
Der Internet Explorer 4 definiert außerdem verschiedene damit zusammenhängende Eigenschaften und Methoden. Die Eigenschaft outerHTML ersetzt den Inhalt eines Elements sowie das Element selbst durch einen angegebenen String mit HTML-Text. Die Eigenschaften innerText und outerText ähneln innerHTML und outerHTML, behandeln den String jedoch als einfachen Text; er wird also nicht als HTML-Text analysiert. Schließlich lassen die Methoden insertAdjacentHTML() und insertAdjacentText() den Inhalt eines Elements unangetastet, fügen aber HTML- bzw. einfachen Text in der Nähe ein (vor oder hinter dem vorhandenen Text, innerhalb oder außerhalb der Tags). Diese Eigenschaften und Funktionen werden weniger häufig verwendet als innerHTML und wurden im Netscape 6 nicht implementiert. Genauere Informationen finden Sie unter »HTMLElement« in der Referenz zu JavaScript auf der Clientseite.
Der Netscape 4 ist weit davon entfernt, den DOM-Standard zu implementieren. Insbesondere stellt er keine Möglichkeiten bereit, Attribute beliebiger Elemente eines Dokuments abzufragen oder zu setzen. Der Netscape 4 unterstützt natürlich den Level 0 des DOM-API, so daß auf Elemente wie Formulare und Links mit den Arrays forms[] und links[] zugegriffen werden kann. Es gibt jedoch keine allgemeine Möglichkeit, die Kinder dieser Elemente zu durchlaufen oder beliebige Attribute von Elementen zu setzen. Außerdem verfügt der Netscape 4 nicht über die Fähigkeit, den Inhalt des Dokuments im Falle einer Größenänderung einzelner Elemente neu anzuordnen.
Bei allen Beschränkungen stellt der Netscape 4 eine API bereit, die den Zugriff auf die wesentlichen »dynamischen Elemente«, mit denen DHTML-Effekte implementiert werden können, (und deren Veränderung) erlaubt. In der API des Netscape 4 heißen diese Elemente Layer; sie schweben über dem Rest des Dokuments und können unabhängig von anderen Elementen des Dokuments verschoben werden. Ebenso können ihre Größe und ihr Inhalt geändert werden. Layer werden üblicherweise unter Verwendung von CSS-Stylesheets implementiert; daher wird die Layer-API des Netscape 4 ausführlich in Kapitel 18 vorgestellt.
Im Folgenden geben wir lediglich einen Überblick darüber, wie Layer-Elemente in einem Dokument erzeugt und wie auf sie (auch schreibend) zugegriffen werden kann. Obwohl der Netscape 4 nichts dem DOM-Standard Vergleichbares bietet, ermöglicht die Layer-API dennoch die Erzeugung einiger der dynamischen Effekte, die mit der Standard-API möglich sind. Die Layer-API wurde dem W3C als Vorschlag vorgelegt, um ein Teil des DOM-Standards zu werden; jedoch wurde kein Teil dieser API jemals standardisiert. Da der Netscape 6 eine komplette Neu-Implementation ist, wurde die Layer-API dort aufgegeben; sie wird im Netscape 6 (und im Mozilla-Browser) nicht unterstützt.
Layer können in einem Dokument mit dem <layer>-Tag erzeugt werden, einer proprietären Erweiterung von Netscape gegenüber HTML. Meist werden Sie in Dokumenten für den Netscape 4 Layer jedoch mit den standardisierten Positionierungsattributen von CSS erzeugen (die in Kapitel 18 besprochen werden). Jedes mit CSS-Stilattributen dynamisierte Element wird vom Netscape 4 als Layer behandelt und kann unter Verwendung der Layer-API bearbeitet werden. (Beachten Sie jedoch, daß im Netscape 4 nicht beliebige Elemente dynamisiert werden können. Um sicherzugehen, werden zu dynamisierende Elemente meist mit einem <div>-Element umgeben.) In JavaScript können Sie ebenfalls dynamisch Layer erzeuen. Dies erfolgt durch den Konstruktor Layer(), über den Sie mehr in der Referenz zu JavaScript auf der Clientseite erfahren.
Wenn Sie ein dynamisches Element, also einen Layer, in Ihrem Dokument erzeugt haben, können Sie im Netscape 4 dank einer einfachen Erweiterung der API des DOM Level 0 darauf zugreifen. So wie Sie auf Formularelemente mit einem Array forms[] und auf Bildelemente mit einem Array images[] zugreifen können, greifen Sie auf Layer mit einem Array layers[] des Document-Objekts zu. Wenn der erste Layer in einem Dokument ein name-Attribut mit dem Wert »layer1« besitzt, können Sie auf dieses Layer-Element mit einem der folgenden Ausdrücke zugreifen:
document.layers[0] // Adressieren des Arrays durch eine Zahl document.layers['layer1'] // Adressieren des Arrays durch den Elementnamen document.layer1 // Benannte Layer werden zu Dokument-Eigenschaften
Wenn ein Layer kein name-Attribut, wohl aber ein id-Attribut besitzt, wird der Wert dieses Attributs statt dessen als Name des Layers verwendet.
Layer werden in Ihren Dokumenten durch Layer-Objekte dargestellt, die zahlreiche nützliche Eigenschaften und Methoden definieren, mit denen Sie Layer verschieben, anzeigen und verbergen können. Sie können die Größe von Layern ändern sowie die Reihenfolge, in der sie übereinanderliegen. Diese Eigenschaften und Methoden hängen eng mit CSS-Stilattributen zusammen und werden daher in Kapitel 18 vorgestellt. Das Interessanteste am Layer-Objekt ist die Tatsache, daß es ein eigenes Document-Objekt enthält: Der Inhalt eines Layers wird als eigenständiges Dokument unabhängig von dem Dokument behandelt, das den Layer enthält. Dadurch können Sie den von einem Layer angezeigten Inhalt modifizieren, indem Sie den Inhalt des Layers mit den Methoden document.write() und document.close() dynamisch neu schreiben. Sie können in einen Layer mit der Methode load() des Layer-Objekts auch dynamisch Dokumente laden. Beachten Sie schließlich noch, daß Layer selbst wiederum Layer enthalten dürfen. Auf solche verschachtelten Layer können Sie mit Ausdrücken wie dem folgenden zugreifen:
// Der zweite Layer im Layer namens "meinLayer" document.meinLayer.document.layers[1];
Bislang haben wir uns in diesem Kapitel mit der API des DOM-Kerns befaßt, die grundlegende Methoden für das Durchlaufen und Bearbeiten von Dokumenten bereitstellt. Der DOM-Standard definiert außerdem weitere optionale API-Module, von denen wir die wichtigsten in den folgenden Kapiteln vorstellen wollen. Zwei dieser Module sind hilfreiche APIs, die auf der Kern-API aufbauen. Die API Traversal definiert weitergehende Techniken, mit denen Dokumente durchlaufen werden können, wobei die nicht interessierenden Knoten ausgefiltert werden. Die API Range definiert Methoden, mit denen zusammenhängende Bereiche von Dokumentinhalt bearbeitet werden können, auch wenn dieser Inhalt nicht an einer Knotengrenze beginnt oder endet. Die APIs Traversal und Range werden in den folgenden Abschnitten kurz vorgestellt. Im Referenzteil zum DOM werden sie umfassend dokumentiert. Die Range-API ist im Netscape 6.1 implementiert (sowie teilweise im Netscape 6), die Traversal-API wird voraussichtlich vollständig vom Mozilla 1.0 unterstützt, so daß zukünftige Versionen des Netscape sie ebenfalls unterstützen werden. Zu dem Zeitpunkt, als dieses Buch geschrieben wurde, unterstützte der Internet Explorer keine dieser APIs.
Am Anfang dieses Kapitels haben Sie Techniken zum Durchlaufen des Dokumentenbaums kennengelernt, bei denen nacheinander jeder Knoten rekursiv betrachtet wurde. Diese Technik ist zwar wichtig, jedoch oft zu aufwendig, da wir üblicherweise nicht jeden Knoten eines Dokuments untersuchen wollen. Statt dessen möchten wir vielleicht nur die <img>-Elemente in einem Dokument oder nur die Teilbäume unterhalb von <table>-Elementen betrachten. Die Traversal-API stellt weitergehende Techniken für diese Art eines selektiven Dokumentdurchlaufs bereit. Wie schon erwähnt ist die Traversal-API optional und wurde zu dem Zeitpunkt, zu dem dieses Buch geschrieben wurde, von den wichtigen Browsern der sechsten Generation nicht unterstützt. Ob sie unterstützt wird, können Sie in einem DOM-konformen Browser mit der folgenden Anweisung überprüfen:
document.implementation.hasFeature("Traversal", 2.0) // True, falls sie unterstützt wird
Die API Traversal besteht aus zwei wichtigen Objekten, die jeweils eine unterschiedlich gefilterte Sicht eines Dokuments erzeugen. Das NodeIterator-Objekt stellt eine »flache«, sequentielle Sicht der Knoten in einem Dokument bereit und unterstützt die Filterung. Sie können beispielsweise einen NodeIterator definieren, der sämtlichen Dokumentinhalt außer den <img>-Tags herausfiltert und diese Image-Elemente als Liste präsentiert. Mit den Methoden nextNode() und previousNode() des Node-Iterator-Objekts können Sie diese Liste vorwärts und rückwärts durchlaufen. Beachten Sie, daß NodeIterator es ermöglicht, ausgewählte Teile eines Dokuments ohne Rekursion zu durchlaufen; Sie können einen NodeIterator einfach in einer Schleife durchlaufen, wobei Sie nextNode() wiederholt aufrufen, bis Sie den oder die Knoten finden, an dem bzw. denen Sie interessiert sind, oder bis die Methode den Wert null zurückgibt und somit anzeigt, daß das Ende des Dokuments erreicht wurde.
Das zweite wichtige Objekt der Traversal-API ist TreeWalker. Dieses Objekt stellt ebenfalls eine gefilterte Sicht eines Dokuments bereit und ermöglicht das Durchlaufen des gefilterten Dokuments, indem die Methoden nextNode() und previousNode() aufgerufen werden; dabei wird jedoch der Dokumentenbaum nicht sequentialisiert. TreeWalker erhält die Baumstruktur des Dokuments (wobei sich die Baumstruktur durch die Filterung der Knoten stark ändern kann). Eine Navigation durch den Baum ist mit den Methoden firstChild(), lastChild(), nextSibling(), previousSibling() und parentNode() möglich. Sie sollten TreeWalker anstelle eines NodeIterator verwenden, wenn Sie den gefilterten Baum selbst durchlaufen wollen, anstatt die Knoten sequentiell mit nextNode() in fester Reihenfolge zu besuchen. Außerdem sollten Sie TreeWalker verwenden, wenn Sie beispielsweise einige Teilbäume beim Durchlaufen ignorieren möchten.
Das Document-Objekt definiert die Methoden createNodeIterator() und create-TreeWalker(), mit denen NodeIterator- und TreeWalker-Objekte definiert werden können. Ein sehr praktischer Test, um festzustellen, ob ein Browser die Traversal-API unterstützt, besteht darin, die Existenz dieser Methoden zu überprüfen:
if (document.createNodeIterator && document.createTreeWalker) {
/* Wir können die Traversal-API verwenden */
}
An createNodeIterator() und createTreeWalker() werden dieselben vier Argumente übergeben. Sie geben jedoch einen unterschiedlichen Objekttyp zurück. Das erste Argument ist der Knoten, an dem der Durchlauf beginnen soll. Dieser sollte das Document-Objekt sein, wenn Sie das gesamte Dokument durchlaufen möchten, oder ein beliebiger anderer Knoten, wenn Sie nur einen Teilbaum des Dokuments durchlaufen möchten. Das zweite Argument ist eine Zahl, die die Knotentypen angibt, die NodeIterator und TreeWalker zurückgeben sollen. Dieses Argument wird gebildet, indem man eine oder die Summe mehrerer SHOW_-Konstanten, die vom NodeFilter-Objekt definiert werden, angibt. (Dieses Objekt wird im nächsten Abschnitt vorgestellt.) Das dritte an die beiden Methoden übergebene Argument ist eine optionale Funktion, mit der ein komplexerer Filter angegeben werden kann, der Knoten nicht einfach aufgrund ihres Typs akzeptiert oder ablehnt (auch dies wird im nächsten Abschnitt vorgestellt). Das letzte Argument ist ein Boolescher Wert, der festlegt, ob Entity-Referenz-Knoten im Dokument während des Durchlaufens expandiert werden sollen. Diese Option kann bei der Verwendung mit XML-Dokumenten hilfreich sein; Web-Programmierer, die mit HTML-Dokumenten arbeiten, brauchen sie nicht zu beachten und können hier den Wert false angeben.
Eines der wichtigsten Merkmale von NodeIterator und TreeWalker ist ihre Selektivität: die Fähigkeit, Knoten herauszufiltern, für die Sie sich nicht interessieren. Wie bereits gesagt, geben Sie die Knoten, an denen Sie interessiert sind, mit dem zweiten und (optional) dritten Argument der Methoden createNodeIterator() und createTreeWalker() an. Diese Argumente legen zwei Stufen der Filterung fest. Die erste Stufe akzeptiert Knoten aufgrund ihres Typs - oder weist sie deswegen zurück. Das NodeFilter-Objekt definiert eine numerische Konstante für jeden Knotentyp, so daß Sie die Knotentypen, an denen Sie interessiert sind, durch Addition der entsprechenden Konstanten angeben (oder durch Anwendung des bitweisen Oder-Operators | auf diese Konstanten).
Wenn Sie beispielsweise nur an den Element- und Text-Knoten eines Dokuments interessiert sind, können Sie den folgenden Ausdruck als zweites Argument angeben:
NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT
Wenn Sie nur an Element-Knoten interessiert sind, verwenden Sie:
NodeFilter.SHOW_ELEMENT
Wenn Sie an allen Knoten interessiert sind oder Knoten nicht einfach aufgrund ihres Typs zurückweisen möchten, verwenden Sie die spezielle Konstante:
NodeFilter.SHOW_ALL
Wenn Sie schließlich an allen Knoten außer Kommentaren interessiert sind, verwenden Sie:
~NodeFilter.SHOW_COMMENT
(Sie können in Kapitel 5 nachschlagen, falls Sie die Bedeutung des Operators ~ vergessen haben sollten.) Beachten Sie, daß diese erste Stufe der Filterung auf einzelne Knoten, nicht aber auf ihre Kinder angewandt wird. Wenn das zweite Argument den Wert NodeFilter.SHOW_TEXT besitzt, gibt Ihr NodeIterator oder TreeWalker keine Element-Knoten an Sie zurück, entfernt sie aber auch nicht vollständig; er durchläuft trotzdem den Teilbaum unterhalb des Element-Knotens, um dort befindliche Text-Knoten zu finden, an denen Sie ja interessiert sind.
Jeder Knoten, der diese typbasierte Filterung besteht, kann einer zweiten Stufe der Filterung unterzogen werden. Dieser zweite Filter wird durch eine von Ihnen zu definierende Funktion implementiert und kann daher beliebig komplex sein. Wenn Sie diese Art der Filterung nicht benötigen, können Sie einfach null als Wert des dritten Arguments an create-NodeIterator() bzw. createTreeWalker() übergeben. Wenn Sie diese Art der Filterung jedoch durchführen möchten, müssen Sie eine Funktion als drittes Argument übergeben.
Die Funktion sollte als einziges Argument einen Knoten erwarten, diesen auswerten und einen Wert zurückgeben, der anzeigt, ob der Knoten herausgefiltert werden soll. Die drei möglichen Rückgabewerte werden durch drei NodeFilter-Konstanten definert. Wenn Ihre Filterfunktion den Wert NodeFilter.FILTER_ACCEPT zurückgibt, wird der Knoten von dem NodeIterator bzw. TreeWalker zurückgegeben. Wenn Ihre Funktion den Wert NodeFilter.FILTER_SKIP zurückgibt, wird der Knoten herausgefiltert und vom NodeIterator bzw. TreeWalker nicht zurückgegeben. Die Kinder des Knotens werden dennoch durchlaufen. Wenn Sie mit einem TreeWalker arbeiten, kann Ihre Filterfunktion auch den Wert NodeFilter.FILTER_REJECT zurückgeben, durch den angezeigt wird, daß der Knoten nicht zurückgegeben und auch nicht weiter durchlaufen werden soll.
Beispiel 17-10 zeigt die Erzeugung und Verwendung eines NodeIterator und hilft Ihnen hoffentlich, das zuvor Beschriebene zu verstehen. Beachten Sie jedoch, daß keiner der wichtigen Webbrowser die Traversal-API unterstützte, als diese Zeilen geschrieben wurden, so daß dieses Beispiel ungetestet ist!
// Definiere eine NodeFilter-Funktion, die nur <img>-Elemente akzeptiert function imgfilter(n) { if (n.tagName == 'IMG') return NodeFilter.FILTER_ACCEPT; else return NodeFilter.FILTER_SKIP; } // Erzeuge einen NodeIterator, um <img>-Tags zu finden var bilder = document.createNodeIterator(document, // Durchlaufe das gesamte Dokument /* Nur Element-Knoten betrachten */ NodeFilter.SHOW_ELEMENT, /* Alles außer <img> ausfiltern */ imgfilter, /* Unbenutzt bei HTML-Dokumenten */ false); // Durchlaufe alle Bilder in einer Schleife, und stelle etwas mit ihnen an var bild; while((bild = bilder.nextNode()) != null) { bild.style.visibility = "hidden"; // Verarbeite hier das Bild }
Die DOM-Range-API besteht aus einem einzelnen Interface: Range. Ein Range-Objekt stellt einen zusammenhängenden Bereich8 von Dokumentinhalt zwischen einer angegebenen Anfangsposition und einer angegebenen Endposition dar. Bei vielen Anwendungen, die Text oder Dokumente anzeigen, kann der Benutzer einen Teil des Dokuments durch Ziehen mit der Maus auswählen. Ein solcher selektierter Teil eines Dokuments entspricht von der Idee her einem Bereich.9 Wenn ein Knoten eines Dokumentenbaums vollständig in einem Bereich liegt, sagen wir auch, dieser Knoten sei »selektiert«, obwohl das Range-Objekt nicht unbedingt etwas mit einer Auswahl des Benutzers zu tun haben muß. Wenn die Anfangs- und Endposition eines Bereichs identisch sind, nennen wir den Bereich »kollabiert«. In diesem Fall repräsentiert das Range-Objekt eine einzelne Position oder einen Einfügepunkt in einem Dokument.
Das Range-Objekt stellt Methoden zur Definition der Anfangs- und Endposition eines Bereichs, zum Kopieren und Löschen des Bereichsinhalts sowie zum Einfügen von Knoten an der Anfangsposition des Bereichs zur Verfügung. Die Unterstützung der Range-API ist optional. Zu dem Zeitpunkt, als dieses Buch geschrieben wurde, wurde sie bereits vom Netscape 6.1 unterstützt. Der Internet Explorer 5 unterstützt eine proprietäre API, die der Range-API ähnelt, aber nicht kompatibel zu ihr ist. Mit der folgenden Programmzeile können Sie überprüfen, ob die Range-API unterstützt wird:
document.implementation.hasFeature("Range", "2.0"); // True, wenn Range unterstützt wird
Die Anfangs- und Endpositionen eines Bereichs werden jeweils durch zwei Werte angegeben. Der erste Wert ist ein Dokument-Knoten, üblicherweise ein Document-, Element- oder Text-Objekt. Der zweite Wert ist eine Zahl, die eine Position innerhalb dieses Knotens angibt. Wenn der Knoten ein Dokument oder Element ist, stellt die Zahl eine Position zwischen den Kindern des Dokuments bzw. Elements dar. Der Wert 0 beispielsweise steht für die Position unmittelbar vor dem ersten Kind des Knotens, der Wert 1 für die Position hinter dem ersten und vor dem zweiten Kind. Wenn der angegebene Knoten ein Text-Knoten (oder ein anderer textbasierter Knotentyp wie Comment) ist, stellt die Zahl eine Position zwischen den Zeichen des Texts dar. Der Wert 0 gibt die Position vor dem ersten Zeichen an, der Wert 1 die Position zwischen dem ersten und zweiten Zeichen usw. Mit derart angegebenen Anfangs- und Endpositionen repräsentiert ein Bereich alle Knoten und/oder Zeichen zwischen der Anfangs- und der Endposition. Was das Range-Interface so nützlich macht, ist die Tatsache, daß die Anfangs- und die Endposition in verschiedenen Knoten des Dokuments liegen können und ein Bereich daher mehrere Element- und Text-Knoten umfassen kann (und diese nur in Teilen).
Um die Funktionsweise der verschiedenen Methoden zur Manipulation von Bereichen zu demonstrieren, werde ich die in der DOM-Spezifikation verwendete Notation übernehmen, um den von einem Bereich repräsentierten Dokumentinhalt zu veranschaulichen. Der Dokumentinhalt wird in Form von HTML-Quelltext gezeigt, wobei Bereichsinhalte in fetter Schrift dargestellt werden. Beispielsweise repräsentiert die folgende Zeile einen Bereich, der an der Position 0 im <body>-Knoten beginnt und bis zur Position 5 im Text-Knoten innerhalb des <h1>-Knotens reicht:
<body><h1>Titel des Dokuments</h1><body>
Um ein Range-Objekt zu erzeugen, rufen Sie die Methode createRange() des Document-Objekts auf:
var r = document.createRange();
Für neu erzeugte Bereiche werden die Anfangs- und Endpunkte mit der Position 0 im Document-Objekt initialisiert. Bevor Sie irgend etwas Interessantes mit einem Bereich anfangen können, müssen Sie die Anfangs- und Endposition auf den gewünschten Dokumentbereich festlegen. Dies können Sie auf verschiedene Weise tun. Die allgemeinste Möglichkeit besteht in einem Aufruf der Methoden setStart() und setEnd(). Beiden wird ein Knoten sowie eine Position in dem Knoten übergeben.
Auf höherer Ebene können Sie die Anfangs- und/oder Endposition festlegen, indem Sie die Methoden setStartBefore(), setStartAfter(), setEndBefore() oder setEndAfter() aufrufen. Diese Methoden nehmen jeweils einen einzelnen Knoten als Argument entgegen. Sie setzen die Anfangs- bzw. Endposition des Bereichs auf die Position vor oder hinter dem angegebenen Knoten innerhalb des Elternknotens.
Wenn Sie einen Bereich definieren möchten, der ein einzelnes Node-Objekt oder einen Teilbaum eines Dokuments repräsentiert, können Sie die Methoden selectNode() und selectNodeContent() verwenden, die beide einen Knoten als einziges Argument entgegennehmen. selectNode() setzt die Anfangs- und Endposition vor und hinter den angegebenen Knoten innerhalb des Elternknotens, wodurch ein Bereich definiert wird, der den Knoten und alle seine Kinder enthält. selectNodeContent() setzt den Anfang des Bereichs auf die Position vor dem ersten Kind des Knotens und das Ende des Bereichs auf die Position hinter dem letzten Kind. Der Bereich, der sich dadurch ergibt, enthält alle Kinder des angegebenen Knotens, nicht jedoch den Knoten selbst.
Wenn Sie einen Bereich definiert haben, können Sie einige interessante Dinge mit ihm anstellen. Um den Dokumentinhalt in einem Bereich zu löschen, rufen Sie einfach die Methode deleteContents() des Range-Objekts auf. Wenn ein Bereich Text-Knoten enthält, die nur teilweise selektiert wurden, ist die Löschung etwas kompliziert. Betrachten Sie den folgenden Bereich:
<p>Dies ist <i>nur</i> ein Test
Nach einem Aufruf von deleteContents() sieht der betroffene Teil des Dokuments folgendermaßen aus:
<p>Dies<i>r</i> ein Test
Obwohl also das <i>-Element (teilweise) im Bereich lag, bleibt dieses Element (mit geändertem Inhalt) auch nach der Löschung im Dokumentenbaum.
Wenn Sie den Inhalt eines Bereichs aus einem Dokument entfernen, ihn aber (vielleicht für eine später erfolgende Einfüge-Operation) speichern möchten, sollten Sie die Metho-de extractContents() anstelle von deleteContents() verwenden. Diese Methode entfernt Knoten aus dem Dokumentenbaum und fügt sie in ein von ihr zurückgegebenes DocumentFragment-Objekt ein (das Sie weiter oben in diesem Kapitel kennengelernt haben). Wenn ein Bereich einen nur teilweise selektierten Knoten enthält, bleibt dieser Knoten im Dokumentenbaum; sein Inhalt wird in der erforderlichen Weise modifiziert. Eine Kopie des Knotens (siehe Node.cloneNode()) wird erstellt (und modifiziert) und in das DocumentFragment-Objekt eingefügt. Wenn in dem obigen Beispiel die Methode extractContents() anstelle von deleteContents() aufgerufen wird, ist der Effekt im Dokument der gleiche wie oben beschrieben. Das zurückgegebene DocumentFragment-Objekt enthält:
ist <i>nu</i>
extractContents() funktioniert, wenn Sie eine Ausschneide-Operation in einem Dokument durchführen möchten. Wenn Sie statt dessen eine Kopier-Operation durchführen möchten - also Inhalt extrahieren, ohne ihn zu löschen -, verwenden Sie die Methode cloneContents() anstelle von extractContents().10
Mit der Anfangsposition eines Bereichs können Sie nicht nur den Textausschnitt festlegen, der gelöscht oder kopiert werden soll, sondern auch einen Punkt festlegen, an dem Text in ein Dokument eingefügt werden soll. Die Methode insertNode() eines Bereichs fügt den angegebenen Knoten (und alle seine Kinder) in das Dokument an der Anfangsposition des Bereichs ein. Wenn der angegebene Knoten bereits Teil des Dokumentenbaums ist, wird er von seinem derzeitigen Ort entfernt und an der durch den Bereich angegebenen Position wieder eingefügt. Wenn der angegebene Knoten ein DocumentFragment-Objekt ist, werden alle Kinder des Knotens anstelle des Knotens selbst eingefügt.
Eine weitere nützliche Methode des Range-Objekts ist surroundContents(). Diese Methode weist dem Inhalt eines Bereichs den angegebenen Knoten als neuen Elternknoten zu und fügt diesen in den Dokumentenbaum an der Position des Bereichs ein. Wenn Sie beispielsweise einen neu erzeugten <i>-Knoten an surroundContents() übergeben, könnten Sie den Bereich
Dies ist nur ein Test
Dies ist <i>nur</i> ein Test
Da die öffnenden und schließenden Tags in HMTL-Dateien sauber geschachtelt werden müssen, kann surroundContents() nicht für Bereiche verwendet werden, die einen nur teilweise selektierten Nicht-Text-Knoten enthalten; in diesem Fall erzeugt die Methode eine Exception. Der im Beispiel zur Methode deleteContents() verwendete Bereich könnte beispielsweise nicht für surroundContents() verwendet werden.
Das Range-Objekt besitzt außerdem verschiedene andere Merkmale. Sie können die Grenzen zweier verschiedener Bereiche mit compareBoundaryPoints() vergleichen, einen Bereich mit cloneRange() kopieren und eine Kopie mit einfachem Text des Inhalts eines Bereichs (ohne jegliches Markup) mit toString() extrahieren. Die Anfangs- und Endposition eines Bereichs kann mit den nur-lesbaren Eigenschaften startContainer, startOffset, endContainer und endOffset abgefragt werden. Die Start- und Endpunkte aller gültigen Bereiche besitzen irgendwo im Dokumentenbaum einen gemeinsamen Vorfahren, der im Zweifelsfall das Document-Objekt an der Wurzel des Baums selbst ist. Auf den jüngsten gemeinsamen Vorfahren können Sie mit Hilfe der Eigenschaft commonAncestorContainer des Bereichs zugreifen.
© 2002, O'Reilly Verlag GmbH & Co. KG