Unter einem synchronisierbaren Tree verstehe ich einen Tree, der sich nicht nur mit Klicks innerhalb des Baumes öffnet und schliesst, sondern bei dem die Äste auch von aussen geöffnet und geschlossen werden können. Auslöser für die vorliegende Technik waren die Anforderungen eines Webshops: Einerseits sollten die Kategorien eines Artikelkatalogs mit beliebiger Verschachtelungstiefe als Navigationsbaum dargestellt werden. Andererseits sollte auch über eine statische Menüleiste sowie eine Bildnavigation in die Katalognavigation eingesprungen werden können. Der vorliegende Beitrag stellt nur den HTML- und JavaScript-Teil der Lösung vor, auf die serverseitige Aufbereitung des Baumes aus den Daten einer Oracle-Datenbank verzichte ich hier. Für das JavaScript verwende ich das Framework JQuery in der Version 1.3.2.
Einbindung von JQuery
Im Header der HTML-Seite muss natürlich JQuery eingebunden werden. Die vorliegende Lösung beruht auf JQuery allein, es ist nicht nötig, weitere JQuery-Pakete wie jquery.treeview einzubinden.
<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
HTML-Tree mit DIV-Tags
Für den HTML-Teil des Baumes verwende ich nicht die Listen-Tags, sondern DIV-Elemente. Die Überlegung dahinter war, mir das Leben zu vereinfachen, indem ich den ganzen Baum aus einer einzigen Art von Tags aufbaue. Äste und Blätter werden dabei danach unterschieden, ob es noch ein verschachteltes Unterelement gibt oder nicht. Mit den Selektoren von JQuery lässt sich das sehr bequem realisieren.
Der Baum sieht in HTML folgendermassen aus:
<div style="border: 1px solid rgb(204, 204, 204); padding: 10px;" > <div id="ds_1">Item 1 <div id="ds_1_1">Item 1_1</div> <div id="ds_1_2">Item 1_2 <div id="ds_1_2_1">Item 1_2_1 <div id="ds_1_2_1_1">Item 1_2_1_1 <div id="ds_1_2_1_1_1">Item 1_2_1_1_1</div> </div> <div id="ds_1_2_1_2">Item 1_2_1_2</div> </div> <div id="ds_1_2_2">Item 1_2_2</div> <div id="ds_1_2_3">Item 1_2_3</div> </div> <div id="ds_1_3">Item 1_3</div> </div> <div id="ds_2">Item 2 <div id="ds_2_1">Item 2_1</div> <div id="ds_2_2">Item 2_2 <div id="ds_2_2_1">Item 2_2_1</div> <div id="ds_2_2_2">Item 2_2_2</div> <div id="ds_2_2_3">Item 2_2_3</div> </div> <div id="ds_2_3">Item 2_3</div> </div> <div id="ds_3">Item 3 <div id="ds_3_1">Item 3_1</div> <div id="ds_3_2">Item 3_2 <div id="ds_3_2_1">Item 3_2_1</div> <div id="ds_3_2_2">Item 3_2_2</div> <div id="ds_3_2_3">Item 3_2_3</div> </div> <div id="ds_3_3">Item 3_3</div> </div> <div id="ds_4">Item 4 <div id="ds_4_1">Item 4_1</div> <div id="ds_4_2">Item 4_2</div> </div> <div id="ds_5">Item 5 </div> </div>
Man benötigt nichts als die folgenden Zutaten:
- Ein Root-Element mit einer eindeutigen Klasse, hier
<div...>...</div>
- Viele verschachtelte Div-Elemente mit einer eindeutigen id und dem angezeigten Text. Wenn die Inhalte wie bei einem Artikelkatalog aus einer Datenbank stammen, dann nimmt man als id am Besten den Primärschlüssel.
CSS
Diesen Div-Tags wird nachher dynamisch eine Klasse zugewiesen. Es gibt drei Klassen:
- ds_close: Alle Äste, die geschlossen sind
- ds_open: Alle Äste, die geöffnet sind
- ds_leaf: Alle Blätter (Childs)
Ob ein Element angezeigt wird oder nicht, ist im zugehörigen CSS geregelt:
<style type="text/css"> /* Style für das Root-Element des Baumes */ div.ds_katgliederung div { padding-left:16px; } div.ds_katgliederung div.ds_close { cursor:pointer !important; } div.ds_katgliederung div.ds_open { cursor:pointer !important; } /* Alle Div-Tags innerhalb eines geschlossenen Astes werden nicht angezeigt */ div.ds_katgliederung div.ds_close div { display:none; } div.ds_katgliederung div.ds_leaf { cursor:default; } </style>
JQuery-Code
Nun fehlt uns nur noch der JavaScript-Code. Er besteht aus zwei Funktionen und einem Event-Handler. Die Idee ist, dass man bei jedem Klick zuerst einmal bestimmt, welche Div-Tags Äste und welche Blätter sind, die entsprechenden Klassen anhängt, und anschliessend alle Äste schliesst. Zuständig dafür ist die Funktion ds_init().
function ds_init() { //setzt für alle Blätter bzw. Childs die Klasse ds_leaf $('div.ds_katgliederung div').not('div:has(div)').attr("class", "ds_leaf"); //setzt alle Äste auf Klasse ds_close //-> im Anfangszustand sind alle Äste geschlossen $('div.ds_katgliederung div:has(div)').attr("class", "ds_close"); return true; }
Dieses brachiale Vorgehen, alle DIV-Tags bei jedem Klick neu zu initialisieren, ist notwendig, weil es sich ja um einen dynamischen Baum handelt, der mit JavaScript und AJAX verändert werden kann. Da somit aus einem Blatt plötzlich ein Ast werden kann, muss man die Klassen bei jedem Klick neu bestimmen.
Die zweite Funktion namens ds_toggleNavigation öffnet und schliesst die Äste. Als Parameter übergibt man ihr die eindeutige id des Elementes, das geöffnet oder geschlossen werden soll. Diese Funktion ruft zuerst ds_init() auf, öffnet dann alle Elternelemente des übergebenen Tags und öffnet schliesslich noch das Element selbst, falls es geschlossen ist. Die Umkehrung, das Schliessen eines geöffneten Elementes, ist nicht nötig, das wird bereits in ds_init erledigt.
function ds_toggleNavigation(id) { var tag = $("#" + id); var tagclass = tag.attr("class"); ds_init(); tag.parents("div.ds_close").attr("class", "ds_open"); if (tagclass == "ds_close") { tag.attr("class", "ds_open"); } return true; }
Nun benötigen wir noch den Event-Handler, der auf das OnClick-Ereignis reagiert. Dieser wird ebenso wie die erste Initialisierung innerhalb von $(document).ready platziert. Beim Eventhandler gilt es zu beachten, dass live(..) verwendet wird, damit auch auf dynamisch zum Baum hinzugefügte Ereignisse reagiert werden kann.
$(document).ready(function() { //Meine ds_katgliederung ds_init(); $('div.ds_katgliederung div').live('click', function(evt) { var tag = $(this); //alert(tag.attr("id")); ds_toggleNavigation(tag.attr("id")); evt.stopImmediatePropagation(); return true; }); });
Damit haben wir eigentlich alles, was zum Baum gehört, beisammen. Allerdings habe ich zwei Behauptungen aufgestellt, die ich noch kurz beweisen möchte:
- Der Baum lässt sich nicht nur mit Klicks innerhalb, sondern auch von aussen öffnen und schliessen. Um das zu zeigen spendieren Sie der Seite die folgenden Links und klicken darauf:
<a href="#" onclick="ds_toggleNavigation('ds_1_2_1')">Toggle 1_2_1</a><br/> <a href="#" onclick="ds_toggleNavigation('ds_2_2')">Toggle 2_2</a><br/> <a href="#" onclick="ds_toggleNavigation('ds_2')">Toggle 2</a>
- Die Navigation funktioniert auch dann, wenn Elemente dynamisch verändert, angehängt oder gelöscht werden. Der folgende JQuery-Code hängt einem mit der übergebenen Id bestimmten Element ein neues Blatt-Element an.
function ds_append(id) { var tag = $("#" + id); var laufnr = 1; if (tag.children()) { laufnr = tag.children().length + 1; } var parent = 0; //alert("id " +id ); var position = id.lastIndexOf("_"); parent = id.substr(position + 1); //alert("Laufnr : " + laufnr + "\nParent: " + parent); var childId = parent + "_" + laufnr; tag.append("<div id=\"ds_" + childId +"\">Item " + childId + "</div>"); ds_init(); }
Um die Funktion aufzurufen, binden Sie ebenfalls einen Link ein:
<a href="#" onclick="ds_append('ds_5')">Append to 5</a><br/>
Jedes Mal, wenn Sie auf diesen Link klicken, wird dem Element mit id „ds_5“ ein weiteres Blatt angehängt. Beim ersten Klick wird aus dem Blatt sofort ein Ast, der nun seinerseits auf Klicks reagiert.