Die UI-Widgets von jQuery lassen sich relativ einfach durch neue Funktionalitäten erweitern, ohne direkt in den Code eingreifen zu müssen. Der Hauptgrund für diese Erweiterbarkeit ist – neben der Grundstruktur als eigene JS-Klasse – die häufige Verwendung von Events um Zustandsänderungen der Objekte nach außen bekannt zu machen. Daneben stellen einige UI-Widgets eine meiner Meinung nach verbesserungswürdige Plugin-API zur Verfügung.
Durch die Verwendung von Events können Dritte sowie Widget-Autoren einigen Widgets auf einfach Weise – nämlich durch Hinzufügen von Event-Handlern – zusätzliche Funktionalitäten mitgeben, ohne dass eine Vermischung von Widget-Code und Zusatzfunktionalität stattfindet. Das Verwenden von Events ist gegenüber Callback-Funktionen insofern vorteilhaft, da sie häufig wesentlich flexibler und wartungsfreundlicher eingesetzt werden können.
Die Erweiterungsaufgabe
Die Erweiterung basiert auf unserer Canvasmap. Das Tutorial zur Canvasmap sollte also bereits bekannt sein. Wir wollen nun unsere Canvasmap dahingehend erweitern, dass wenn der User einen Teil unserer Imagemap anklickt, nicht nur der Teil hervorgehoben wird, sondern auch eine hübsche Info-Box über der Karte mit weiteren Informationen erscheint. In dieser Box soll sich desweiteren noch ein „schließen-Button“ befinden.
Außerdem stellen wir als Scripter fest, dass sich auf der umzusetzenden Seite ein weiteres Widget befindet, welches ebenfalls eine Info-Box öffnet und wir daher versuchen wollen diese Box-Logik für beide Widgets zu verwenden. (Letztere Anforderung führt dazu, dass wir beim Scripten etwas umständlicher vorgehen, jedoch können wir dann den Code besser wiederverwenden.)
Diese Aufgabe habe ich versucht auf 2 verschiedene Wege zu lösen. Die Demos sind hier und hier.
Events in unseren Canvasmap-Code einbauen
Als erstes müssen wir dafür sorgen, dass unser Canvasmap-Widget besagte Events wirft, mit denen wir arbeiten können. Hierzu fügen wir eine Methode (propagate) hinzu, die diese Events mit weiteren Informationen wirft.
propagate: function(n, e, extraData){
e = e ||
{type: n, preventDefault: true};
var args = [e, $.extend({}, this.ui(), extraData)];
this.element.triggerHandler(this.widgetName + n, args);
}
Diese Methode erwartet als 1. Parameter den Namen des zu triggernden Events (diesem Namen wird zusätzlich der Name des Widgets vorangestellt), als 2. optionalen Parameter ein Eventobjekt und als 3. optionalen Parameter weitergehende Informationen, welche unser Canvasmap-Objekt mit zusätzlichen optionalen Informationen erweitert.
Wir können nun diese Methode an verschiedenen Stellen unseres Scripts aufrufen, um Events und Zustandsänderungen nach außen bekannt zu geben. Wir rufen diese Methode unter anderem in unserer Click-Handler und am Ende unserer init-Methode auf:
Click-Handler:
that.propagate.call(that, 'click', e, {area: that.clickArea});
Init-Methode:
this.propagate('init');
Unseren Canvasmap Click Handler könnnten wir dann wie folgt binden:
$('#map').bind('canvasmapclick', onMapClick);
Der Eventhandler bekommt als 1. Parameter das Eventobjekt und als 2. Parameter unser evtl. erweitertes Widget-Objekt.
Das Functional-Pattern
Das Functional-Pattern ist eine sehr schöne und einfache Art seinen Code zu organisieren. Wie der Name bereits sagt wird eine Funktion verwendet, um seinen Code zu kapseln. Die bekannteste, aber zugleich wohl komplizierteste Variante stellt das sog. Module-Pattern dar, mit welchem Singeltons erstellt werden können. Ich persönlich bevorzuge hier die Syntax des sog. Revealing Module Pattern.
Die simpleste Form des Functional-Patterns könnte so aussehen (Ob man diese bereits als Functional-Pattern bezeichnen darf, weiß ich ehrlich gesagt nicht):
function erstelleVerhaltenA(){
var links = $('a.verhalten-a');
function show(){
//Event-Handler-Code
}
function nochEineFn(){
//irgendetwas
}
//init-Code
links.click(show);
//...
}
Diese Form eignet sich sehr schön, um kleine Funktionalitäten zu realisieren, die keiner öffentlichen Methoden und Eigenschaften benötigen.
Sollte sich dies Ändern kann der obige Code schnell erweitert werden. Wir möchten beispielsweise, dass die Variable links eine öffentliche Eigenschaft und show eine öffentliche Methode wird, dann könnten wir das wie folgt ausdrücken:
function erstelleVerhaltenA(){
var links = $('a.verhalten-a');
function show(){
//Event-Handler-Code
}
function nochEineFn(){
//irgendetwas
}
//init-Code
links.click(show);
//...
//öffentliche Methoden und Eigenschaften:
return {
links: links,
show: show
};
}
Der Zugriff hierzu erfolgt so:
var verhaltenA = erstelleVerhaltenA();
verhaltenA.links; //=> enthält unsere Links
Anders als beim Module-Pattern kann die Funktion mehrmals aufgerufen werden, welche dann immer einen neuen Ausführungskontext erstellt.
Infobox-Erweiterung mit dem Functional-Pattern
Nun fangen wir mit unserer Info-Box-Erweiterung an. Diese sollte grundsätzlich in der Lage sein alle mit der UI-Widget-Factory erstellten Widgets zu erweitern. Der Grundaufbau sieht wie folgt aus:
$.createDescrBox = function(obj, descrBoxes, opts){
};
$.createDescrBox.defaults = {
closeHTML: '<a href="#" class="close">schliessen</a>',
appendCloseTo: 'div.head'
};
Die Methode createDescrBox bekommt als ersten Parameter ein konkretes UI-Widget-Objekt, ein jQuery-Objekt mit den Info-Boxen sowie die Optionen, welche mit den defaults aufgefüllt werden. Aufgabe der createDescrBox-Methode ist es ein vorhandenes Objekt mit weiteren Eigenschaften und Methoden zu erweitern. Der nachfolgende Code wird komplett in die createDescrBox-Methode geschrieben.
Variabeln, die wir in mehreren Funktionen benötigen
Wir erstellen im gesamten Geltungsbereich der createDescrBox-Funktion einen Cross-Browser-tauglichen Namen für das tabindex-Attribut, eine Variable namens reFocusElm und füllen anschließend die Optionen unseres Widget-Objekts mit unseren Defaults und den übergebenen Optionen auf:
var tabI = ($.browser.msie) ?
'tabIndex' :
'tabindex',
reFocusElm = null;
opts = $.extend(obj.options, $.createDescrBox.defaults, opts);
Die tabI sowie die reFocusElm Variable dient uns später dazu, den Fokus zwischen dem öffnenden Control-Element und der Infobox hin und her zu setzen, um eine bessere Zugänglichkeit zu erreichen.
Hiernach definieren wir die init-Funktion, welche in allen Boxen nach der ersten Überschrift sucht und diese als fokusierbar kennzeichnt (‘-1′ steht für vom Autoren, jedoch nicht vom User fokusierbar).
Abschließend fügen wir unseren Schliessen-Button ein und belegen ihn mit einem Click-Eventhandler. Welcher einerseits ein selfclose-Event tiggert, den Fokus auf das reForcusElm zurücksetzt und abschliessend die close-Funktion aufruft.
function init(){
descrBoxes.each(function(){
var descBox = $(this),
closeParent = $(opts.appendCloseTo, descBox[0]),
head = $(':header:first', descBox[0]),
headID = head
.attr(tabI, '-1')
.attr('id');
if(!headID){
headID = 'header-'+ new Date().getTime();
head.attr('id', headID);
}
descBox
.attr({
'role': 'description',
'aria-hidden': 'true',
'aria-labeledby': headID
});
$(opts.closeHTML)
.appendTo(closeParent[0])
.attr({'role': 'button'})
.bind('click', function(e){
obj.propagate('boxSelfClose', e, {descBox: descBox, closer: this});
if(reFocusElm){
setTimeout(function(){
reFocusElm.focus();
reFocusElm = null;
}, 1);
};
close(descBox);
return false;
});
});
}
Die close-Funktion
Die Schließen-Funktion ist recht simple. Sie triggert ebenfalls ein Close Event mit unserer propagate-Methode, führt eine kleine Fadeout-Animation durch und versteckt schließlich unser Dialog.
function close(elm){
if (elm && elm[0]) {
obj.propagate('boxClose', null, {descBox: elm});
elm
.animate({
opacity: 0,
duration: 200
}, {
complete: function(){
elm.hide();
}
})
.attr({'aria-hidden': 'true'})
.removeClass('active');
}
}
Die open-Funktion
Auch die open-Funktion ist sehr einfach. Als erstes schließen wir evtl. vorhandene andere Infoboxen. Danach triggern wir ein ‘beforeOpenBox’, welches wir zum Beispiel zur Positionierung des sich öffnenden Infobox verwenden können, faden unsere Infobox ein, fokusieren die 1. Überschrift in unserer Box und speichern das öffnende Kontroll-Element in die Variable reFocusElm.
function open(box, opener){
reFocusElm = null;
close(descrBoxes.filter('.active'));
obj.propagate('beforeOpenBox', null, {dialog: box, opener: opener});
box
.addClass('active')
.animate({
opacity: 1
})
.css({display: 'block'})
.attr({'aria-hidden': 'false'});
setTimeout(function(){
$(':header',box[0])[0].focus();
}, 1);
reFocusElm = opener;
}
Aufruf der init-Funktion und Bestimmung der öffentlichen Methoden und Eigenschaften
Abschließend rufen wir die init-Funktion auf und definieren die öffentlichen Methoden. Da wir bereits ein Objekt haben, nämlich das Widget-Objekt, geben wir kein Objekt zurück (wie oben zum Functional-Pattern gezeigt), sondern erweitern dieses einfach:
init();
obj.reFocusElm = reFocusElm;
obj.closeDialog = close;
obj.openDialog = open;
Infobox-Logik in ein Canvasmap-Objekt integrieren und aktivieren
Wir haben nun eine Funktion, welche ein jQuery UI-Widget-Objekt um Methoden zum Erstellen und Öffnen bzw. Schließen von Infoboxen erweitert. Allerdings haben wir weder definiert, daß ein canvasmap-Objekt erweitert werden soll noch wann eine Infobox geöffnet werden soll.
Dies beschreiben wir in unserer nächsten Funktion:
Wir nennen diese Methode createCanvasMapInfoBox. Sie bekommt den Selektor für die Canvasmap sowie die Optionen für die Canvasmap einerseits und die Infobox andererseits übergeben. Innerhalb dieser Funktion sowie der weiteren inneren Funktionen soll desweiteren die Variable obj bekannt sein (Diese Variable wird später eine Referenz auf unser Canvasmap-Objekt enthalten):
$.createCanvasMapInfoBox = function (mapSel, dialogOpts, canvasmapOpts){
var obj;
};
Der gesamte weitere Code wird in diese Methode platziert.
Die init-Funktion
An der init-Funktion lässt sich sehr schön sehen, wie wir die Events unseres Widgets als Hooks nutzen, um die zusätzliche Funktionalität zu implementieren. Wir binden hier zum einen unsere Event-Handler und starten unser canvasmap-Widget:
function init(){
var map = $(mapSel)
.bind('canvasmapinit', onMapInit)
.canvasmap(canvasmapOpts)
.bind('canvasmapclick', onMapClick)
.bind('canvasmapboxSelfClose', onSelfClose);
obj = $.data(map[0], 'canvasmap');
}
Alle weiteren Funktionen sind Eventhandler.
Die onMapInit-Funktion
Die onMapInit-Funktion sammelt im wesentlichen alle Info-Boxen zusammen und ruft hiernach unsere $.createDescrBox – Methode auf.
function onMapInit(e, ui){
var descBoxes = [];
ui.instance.areas.each(function(){
var jElm = $(this),
href = $(this).attr('href');
jElm.attr({'aria-describedby': href.substr(1)});
descBoxes.push($(href)[0]);
});
$.createDescrBox(ui.instance, $(descBoxes), dialogOpts);
}
Die onSelfClose-Funktion
Jedesmal, wenn eine Info-Box geschlossen wird, ohne dass sich eine neue öffnet, wollen wir dass das Highlighting der Canvasmap ebenfalls verschwindet. Hierzu nutzen wir das, durch unsere createDescrBox eingeführte, Event ‘boxSelfClose’.
function onSelfClose(){
obj.clickArea = [];
obj.ctx.clearRect(0, 0, obj.cWidth, obj.cHeight);
return false;
}
Die onMapClick-Funktion
Unser bis jetzt modifiziertes canvasmap-Objekt kennt bereits die Methoden zum Öffnen und schließen von Infoboxen, aber das wichtigste, nämlich wann eine Box geöffnet werden soll, ist noch nicht definiert. Dies erreichen wir durch die abschließende onMapClick-Funktion:
function onMapClick(e, ui){
obj.openDescBox($(ui.area.attr('href')), ui.area[0]);
}
Damit ist unsere Erweiterung praktisch fertig und wir müssen nur noch die init-Funktion aufrufen.
Andere Möglichkeiten der Erweiterung
Das Script (hier nochmal die fertige Demo) oben macht sich die dynamische Natur von JavaScript zu nutze, welche es erlaubt ein bereits erstelltes Objekt durch weitere Eigenschaften und Methoden zu erweitern. Dies bedeutet, dass wir die öffentlichen Methoden unserer Erweiterung auch von außen, so aufrufen können – wie wir es von den UI-Widget auch gewöhnt sind. Nachfolgender Code würde beispielsweise beim Click auf den Button die “Spandau-Infobox” öffnen und fokusieren sowie beim Schließen (nur über selfClose) den Fokus wieder auf den öffnenden Button setzen:
$('button.open').click(function (e){
$('#map').canvasmap('openDescBox', $('#Spandau'), this, e);
});
Daneben ist selbstverständlich auch ein klassischer Weg der Vererbung möglich, bei dem die JS-Klasse selbst erweitert wird bzw. als Erweiterung einer weiteren JS-Klasse dient und dann automatisch erweiterte Objekte kreiert.
Als 4. Demo könnt ihr Euch ein Script anschauen, welche die obige Erweiterungsaufgabe mit dieser klassischen Technik löst. Außerdem wird hier für eine Funktionalität auf die bereits angesprochene Plugin-Architektur der UI-Widgets zurückgegriffen.