protofunc()

widgetExtend: jQuery UI Widgets erweitern

Tags: deutsch, javascript, jquery

Letztendlich gibt es mehrere Möglichkeiten vorhandene jQuery UI Widgets zu erweitern. Die hierzu am häufigsten verwendete Methode ist die $.extend. Was in etwa so aussieht:

//neues a11yTabs erweitert tabs
$.widget('ui.a11yTabs', $.extend({}, $.ui.tabs.prototype, {
	select: function(){
		$.ui.tabs.prototype.select.apply(this, arguments);
	}
});
// tabs selbst erweitern
//altes select sichern
var oldSelect = $.ui.tabs.prototype.select;
$.extend($.ui.tabs.prototype, {
	select: function(){
		oldSelect.apply(this, arguments);
	}
});

Überschreibt man hierdurch eine bereits vorhandene Funktion, welche man noch nutzen möchte, muß man diese, wie im zweiten Beispiel geschehen, vor dem überschreiben zwischenspeichern, so daß man weiterhin auf die Originalmethode Zugriff hat.

Eine relativ elegante Schreibweise hierzu findet sich bei Felix Nagel, welcher folgenden einfachen Code zeigt:

// extends original ui.tabs widget
$.extend($.ui.tabs.prototype,{
	// copy original method
	_original_init: $.ui.tabs.prototype._init,
	// when widget is initiated
	_init: function() {
		var self = this, options = this.options;
		// fire original method
		self._original_init();

		// now we can do some accessibility stuff
	}
});

Wenn man derartiges drei- bis viermal schreiben muß, kommt man sich aber doch recht schnell etwas blöd vor. Als sich mir eben dieses Problem stellte, hatte ich daher folgende kleine extend-Methode geschrieben (noch nicht völlig durchgetestet):

var slice = Array.prototype.slice;
$.widgetExtend = function(widget, exts){
	var args = arguments;

	$.each(exts, function(name, fn){
		if( name in widget ){
			if( fn && $.isFunction(fn) ){
				fn._super = widget[name];
			} else {
				widget['_super'+ name] = widget[name];
			}
		}
		widget[name] = fn;
	});

	if( args.length > 2 ){
		args = slice.call(arguments, 2);
		args.unshift(widget);
		widget = $.widgetExtend.apply(this, args);
	}

	return widget;
};

Mit dieser kleinen Methode kann man, dannn ohne lästiges zwischenspeichern über arguments.callee._super auf die Hauptmethode zugreifen:

//neues a11yTabs erweitert tabs
$.widget('ui.a11yTabs', $.widgetExtend({}, $.ui.tabs.prototype, {
	select: function(){
                //$.ui.tabs.prototype.select kann eigentlich auch noch verwendet werden
		arguments.callee._super.apply(this, arguments);
	}
});
// tabs selbst erweitern

$.widgetExtend($.ui.tabs.prototype, {
	select: function(){
		arguments.callee._super.apply(this, arguments);
	}
});
Written January 9, 2010 by
alexander farkas

mwheelIntent: Das gebrauchstaugliche mouswheel-Event

Tags: Usability, deutsch, javascript, jquery

Eine Möglichkeit die Usability von Javascript Widgets zu erhöhen, ist es eine reichhaltige Interaktionsmöglichkeit zu bieten. D.h. beispielsweise, daß ein Carousel nicht nur durch einen Click auf die Vorwärts-/Rückwärts-Schalter, sondern beispielsweise auch durch Tastatur oder eben das Mausrad bedient werden kann.

Javascript Widgets, die typischwerweise mit dem Mausrad bedient werden können, sind Karten wie Googlemaps, Yahoo Maps, Scrollbar-Ersetzungen sowie Laufbänder/Carousels. Mit dem Hinzufügen einer einfachen Mausradbehandlung zu diesen Widgets verursacht man jedoch in der Regel gleichzeitig ein größeres Usability-Problem. Denn diese Widgets sind in der Regel innerhalb eines Dokuments angelegt, welches ebenfalls mit dem Mausrad bedient werden kann. Möchte der User beispielsweise das Dokument mit dem Mausrad scrollen und kommt dabei eher zufällig auf ein Widget, welches ebenfalls per Mausrad gesteuert werden kann, fängt gerade dieses Widget die Mausradeingabe ab. Dies dürfte vom User nicht gewüsncht sein und ihn mehr verärgern als daß es ihn freut, daß er das Widget auch mit dem Mausrad bedienen kann.

Die Lösung für dieses Problem ist relativ einfach (und ist alleine deshalb bereits genial) und wird beispielsweise in einigen nativen Appliaktionen wie beispielsweise Firefox genutzt, um zu entscheiden, welches Widget gerade mit dem Mausrad bedient werden soll.

Hat der User nämlich in einem bestimmten “Mausradinteraktionsbereich” angefangen das Mausrad zu bedienen, so erhält dieser Bereich das Mausrad-Event exklusiv, solange der User sich a) über diesen Bereich bewegt und b) nicht die Maus bewegt, selbst wenn er dabei über andere interaktive Bereiche scrollt.

Ich habe mir erlaubt für eine solche Behandlung des Mausradevents ein jQuery-Plugin mit mwheelIntent-Demo zu schreiben. Die Nutzung ist relativ einfach:

$('div.widget').bind('mwheelIntent', function(e, d){
	//die mausrad implementierung
}):

Wie wurde dies implementiert

Das Grundgerüst

Für alle die es interessiert ein neues tolles custom-Event für jQuery zu schreiben, hier eine kurze Anleitung am Beispiel des mwheelIntent-Codes.

Als 1. definieren wir einige Variablen, welche wir noch benötigen werden. Besondere Beachtung sollte der Variable mwheelI geschenkt werden. Unter der Eigenschaft pos speichern wir mit jeder Mausradbewegung ab, wo sich das Mausrad gerade innerhalb des Viewports befindet, um zu entscheiden, ob der User die Maus zwischenzeitlich bewegt hat. Anfangs setzen wir diese mit jeweils -260 für x und y Koordinate weit aus dem Viewport, so daß anfangs immer von einer Mausbwegung ausgegangen wird.

Die Variable minDif gibt an, um wieviel Pixel der User die Maus mindestens bewegt haben muß, um eine Änderung des “Mausradinteraktionsbereichs” zu bewirken. Das eigentliche Kernstück unseres Events befindet sich unter $.event.special.mwheelIntent.

Die Methode setup wird aufgerufen, sobald der 1. mwheelIntent-Handler an einem DOM-Objekt gebindet wird, teardown, wenn der letzte mwheelIntent-Handler wieder entfernt wird. Die Methode handler wird letztendlich durch unser Event-System selbst aufgerufen und stößt das Aufrufen der eigentlich hinzugefügten Handler als eine Art proxy-Handler an. Dieser Methodenname könnte – meines Wissens nach – auch anders heißen, es ist jedoch Konvention ihn so zu nennen.

(function($){

//einige variablen
var mwheelI = {
			pos: [-260, -260]
		},
	minDif 	= 3,
	doc 	= document,
	root 	= doc.documentElement,
	body 	= doc.body,
	longDelay, shortDelay
;

// das eigentliche event grundgerüst
$.event.special.mwheelIntent = {
	setup: function(){

    },
	teardown: function(){

    },
    handler: function(e, d){

    }
};

// shortcuts  .bind('mwheelIntent', fn) -> .mwheelIntent()
$.fn.extend({
	mwheelIntent: function(fn) {
		return fn ? this.bind("mwheelIntent", fn) : this.trigger("mwheelIntent");
	},

	unmwheelIntent: function(fn) {
		return this.unbind("mwheelIntent", fn);
	}
});

//initialisierung des scrollbaren bereichs
$(function(){
	// falls body anfangs undefined gewesen sein sollte
	body = doc.body;
	// falls der User das cross-browser-mousewheel-Plugin nicht eingebunden hat
	if(!$.fn.mousewheel){
		setTimeout(function(){
			throw('Please include the mousewheel plugin before the mwheelIntent-plugin');
		}, 0);
	}
	//document als immer scrollbaren bereich berücksichtigen
	$(doc).bind('mwheelIntent.mwheelIntentDefault', function(){});
});
})(jQuery);

jQuery 1.4 wird zudem die beiden neuen Event-Hooks add und remove einführen, welche mit jedem hinzugefügten bzw. jedem entfernten Handler aufgerufen werden. (Mehr zu den neuen jQuery 1.4 Event hooks). Allerdings brauchen wir diese nicht.

Die Mausradbehandlung

Kommen wir nun zu der eigentlichen Implementierung. Unsere setup-Methode fügt letztendlich einen Eventlistener für das normale Mausradevent hinzu, welche unsere Handler Methode aufruft, in dem die Berechnung stattfindet, ob das Mausradevent durchgelassen werden soll oder nicht. Außerdem wird beim Verlassen der Maus aus dem Bereich die Funktion unsetPos aufgerufen, welche unsere eventuell gesetzten Werte auf den Anfangswert zurücksetzt. Bei den Objekten document, html und body wird dieser Listener nicht hinzugefügt, da er hier keinen Sinn machen würde und aufgrund der Tatsache, daß mouseleave ein auf mouseout aufbauendes Special-Event ist, einiges an Performance Kosten würde. Die teardown entfernt beide Listener wieder.

Die handler-Methode verfügt nun über den bereits erwähnten simplen, aber wirkungsvollen Code. Jedesmal wenn der User das Mausrad bewegt, wird die Mausposition abgespeichert und mit der letzten verglichen. Ist das DOM-Objekt mit dem letzten -durch das Mausrad bedienten – Objekt identisch oder hat der User seitdem die Maus weiter bewegt als in minDif definiert, wird das Event durchgelassen und die gebindeten handler mit der Methode $.event.handle.call(this, e, d); aufgerufen.


function unsetPos(){
	if(this === mwheelI.elem){
		mwheelI.pos = [-260, -260];
		mwheelI.elem = false;
		minDif = 3;
	}
}

$.event.special.mwheelIntent = {
	setup: function(){
		var jElm = $(this).bind('mousewheel', $.event.special.mwheelIntent.handler);
		if( this !== doc && this !== root && this !== body ){
			jElm.bind('mouseleave', unsetPos);
		}
		jElm = null;
        return true;
    },
	teardown: function(){
        $(this)
			.unbind('mousewheel', $.event.special.mwheelIntent.handler)
			.unbind('mouseleave', unsetPos)
		;
        return true;
    },
    handler: function(e, d){
		var pos = [e.clientX, e.clientY];
		if( this === mwheelI.elem || Math.abs(mwheelI.pos[0] - pos[0]) > minDif || Math.abs(mwheelI.pos[1] - pos[1]) > minDif ){
            mwheelI.elem = this;
			mwheelI.pos = pos;
			e = $.extend({}, e, {type: 'mwheelIntent'});
            return $.event.handle.call(this, e, d);
		}
    }
};

Verfeinerung der Mausradbehandlung

Dem aufmerksamen Leser wird aufgefallen sein, daß ich die Variable minDif ebenfalls auf 3 zurücksetze, obwohl ich sie gar nicht geändert haben. Außerdem habe ich 2 Variablen (longDelay, shortDelay) deklariert, die noch gar nicht genutzt werden. Dies hat mit einer kleineren Verfeinerung unseres Script zu tun. Hat der User gerade erst das Mausrad gedreht, soll er größere Mausbewegungen machen können, um eine ungewollte Verschiebung der Maus eben durch die Betätigung des Mausrads abzufangen. Dieser Code ist in der handler-Methode untergebracht und sieht wie folgt aus:

handler: function(e, d){
	var pos = [e.clientX, e.clientY];
	if( this === mwheelI.elem || Math.abs(mwheelI.pos[0] - pos[0]) > minDif || Math.abs(mwheelI.pos[1] - pos[1]) > minDif ){
		mwheelI.elem = this;
		mwheelI.pos = pos;
		minDif = 250;

		clearTimeout(shortDelay);
		shortDelay = setTimeout(function(){
			minDif = 10;
		}, 200);
		clearTimeout(longDelay);
		longDelay = setTimeout(function(){
			minDif = 3;
		}, 1500);
		e = $.extend({}, e, {type: 'mwheelIntent'});
		return $.event.handle.call(this, e, d);
	}
}

Im Ergebnis kann der User also 200ms nachdem er das Mausrad bedient hat die Maus um 250 Pixel bewegen, ohne daß dies eine Änderung des durch das Mausrad bedienbaren Bereichs zur Folge hat. Außerdem ist der Bereich für 1.5 Sekunden von 3 auf 10 Pixel erhöht.

Fazit

Die Berücksichtigung von Mausradeingaben kann die Userexpierence und die Bedienbarkeit von Widgets steigern, aber auch gleichzeitig große Usability-Problem verursachen, wenn die Intention des Users nicht berücksichtigt wird. Mit ein bißchen JS kann die wahrscheinliche Intention des Nutzers berechnet werden. (zur mwheelIntent-Projekt-Seite)

Written December 29, 2009 by
alexander farkas

jQuery: live-Methode / Braucht jQuery 1.4 eine neue API für Event Delegation?

Tags: deutsch, javascript, jquery

Jedes jQuery-Major-Release hat wesentliche Neuerungen/Verbesserungen gebracht. Bei jQuery 1.3 war es wohl die Einführung von Sizzle, die Umstellung von Browser-Sniffing auf Feature-Detection und die Einführung der live-Methode für Event Delegation.

Letztendlich habe ich die live-Methode in jQuery 1.3 so gut wie nie genutzt, da ich es für meine Usecases für ineffizient gehalten habe, das document-Objekt mit Eventlistener zu zuknallen. Eine präzisere Möglichkeit Event Delegation in jQuery 1.3 zu nutzen, war für mich immer die Nutzung der closest-Methode:

$('#nav').bind('click', function(e){
	// Der zweite Parameter wird erst in jQuery 1.4 eingeführt!
	var anchor = $(e.target).closest('a', this);
	if(!anchor[0]){return;)
	// mach was mit anchor
});

Das Problem mit der live-Methode

Mit jQuery 1.4 wird Event Delegation und die Möglichkeiten der live-Methode stark erweitert. Zum einen werden Events unterstützt, die nicht bubbeln und zum anderen soll der Entwickler bestimmen können, an welches DOM-Objekt der Eventlistener hinzugefügt wird. Dies dürfte in jedem Fall dazu führen, daß die Methode häufiger eingesetzt wird. Gleichzeitig haben einige Leute in der Vergangenheit gefordert, die live-Mwthode entweder abzuändern oder zumindest eine neue Methode einzuführen. Dies geschah mit besonderer Rücksicht auf Performance und wurde – meiner Meinung nach – zurecht abgelehnt.

Das Problem ist jedoch, daß die live-Methode schwer zu erklären/dokumentieren ist, leicht zu Fehlern führen kann und den Entwickler dazu zwingt bereits beim Instantiieren eines jQuery-Objekts über die spätere Art der Event Delegation nachzudenken. Diese Grundproblematik läßt sich bereits bei jQuery 1.3 ausmachen und wird bei jQuery 1.4 noch komplizierter (aber nicht unbedingt schlimmer).

Problem bei jQuery 1.3

Laut jQuery-Dokumentation kann live u.a. nicht verwendet werden, wenn man beim Instanzieren den Context-Parameter nutzt bzw. wenn man Traversing-Methoden zwischenschaltet. Eine ältere jQuery Dokumentation hat hiervon ausdrücklich die Methode find als nicht funktionierende Traversing Methode ausgenommen. Letztendlich stimmt diese Behauptung in der Dokumentation nicht ganz. Eine Erklärung wann dies funktionieren kann und wann nicht, wäre jedoch einfach zu kompliziert. Hier eine kurze Erklärung

// funktioniert in 1.3 nicht,
$('li', $('#nav')[0]).live('click'....
// ... da
// 1. der Eventlistener dem document-Objekt hinzugefügt wird
// 2. jQuery ausschließlich den 'li', aber nicht den '#nav' Selektor kennt

// funktioniert in 1.3,
$('li', $('#nav')).live('click'....
$('#nav').find('li').live('click'....
// ... da
// jQuery beide Selektor-Bestandteile kennt und diese zu '#nav li' zusammenfassen kann

// funktioniert in 1.3 nicht,
$('li, a', $('#nav')).live('click'....
// ... da jQuery beide Selektor-Bestandteile zu '#nav li, a' zusammenfaßt

Eskalierung des Problems mit jQuery 1.4 (nightly-Stand)

jQuery 1.4 ist noch nicht draußen, aber der derzeitige Stand kann über github eingesehen werden. Zudem existiert eine öffentliche Alpha, welche ich zum Testen herangezogen habe.

Mit dem derzeitigen Stand von jQuery 1.4a1 wird die oben beschriebene Ungenauigkeit zu einem großen Problem, denn die Verwendung des context-Parameters soll nicht nur ermöglicht werden, sondern wird zu einem elementaren Feature, da nun der context-Parameter bestimmen soll, an welchem Element der Eventhanlder gebunden werden soll. Das Problem hierbei ist jedoch eben die Tatsache, daß es eine Reihe von Bedingungen auftreten müssen, damit das ganze funktioniert.

//Event wird in der Regel an nav-Element gebunden und funktioniert in der Regel wie gewünscht
$('li', $('#nav')[0]).live('click'....
// aber dann nicht, wenn $('#nav')[0] === undefined ist

//Event wird an document-body gebunden (Obwohl ein context-Parameter angegeben wurde, funktioniert aber ansonsten wie gewünscht.)
$('li', $('#nav')).live('click'....

//Funktioniert wie bei jQuery 1.3 nicht wie gewünscht
$('li, a', $('#nav')).live('click'....

Die Dokumentation müßte hier einerseits auf die Unterscheidung hinweisen, was alleine für sich bereits ziemlich verwirrend sein könnte und andererseits darauf, daß die 1. Zeile fehleranfällig ist. Kommt nämlich auf der gesamten Seite kein #nav-Element vor, ist $(‘#nav’)[0] undefined, was dazu führt, daß erstens der Eventhandler zum document.body hinzugefügt und zweitens der Eventhandler bei jedem Click auf/in irgend ein li aufgerufen wird und damit nicht so funktioniert wie gewünscht.

Das gesamte Problem wird zusätzlich verschärft, wenn man einen Eventhandler mehreren Elementen hinzufügen möchte.

//Event wird an das document.body gebunden und funktioniert wie gewünscht
$('div.teaser', $('div.teaser-wrapper')).live('click'...

//Schreibweise wird von jQuery nicht unterstützt, Event wird an document.body gebunden und jeder Click auf/in ein div.teaser löst Event aus
$('div.teaser', $('div.teaser-wrapper').get()).live('click'...

Funktionierender Code, der das neue Event Delegation-Feature von jQuery 1.4 nutzt und dabei sowohl klar, verständlich als auch robust ist, könnte beispielsweise wie folgt aussehen:

$('#nav').each(function(){
	$('a', this).live('click' ...
});
//oder
$('div.teaser-wrapper').each(function(){
	$('div.teaser', this).live('click' ...
});

Letztendlich sind wir damit ziemlich nah an dem dran, was wir bereits in jQuery 1.3 schreiben können, um Event Delegation mit anderem context als document bzw. document.body zu nutzen, nämlich:

$('#nav').bind('click', function(e){
	// Der zweite Parameter wird erst in jQuery 1.4 eingeführt!
	var anchor = $(e.target).closest('a', this);
	if(!anchor[0]){return;)
	// mach was mit anchor
});

Es ist ziemlich “jQuery-unlike” mehrere Zeilen-Code zu schreiben, um nur eine “einfache” Sache zu erreichen, aber genau das würde bei einer Beibehaltung der derzeitigen API passieren. Ganz abgesehen davon, daß die Dokumentation um weitere kompliziertere Erklärungen nicht umhin kommen würde.

Eine einfache Implementierung einer neuen jQuery-Event Delegation API könnte hierbei beispielsweise wie folgt aussehen.

(function($){
	var dummy = $([]);

	$.each({addLive: 'live', removeLive: 'die'}, function(name, jMethod){
		$.fn[name] = function(sel){
			var args = (this[0]) ? Array.prototype.slice.call(arguments, 1) : [];
			return this.each(function(){
				dummy.selector = sel;
				dummy.context = this;
				$.fn[jMethod].apply(dummy, args);
			});
		};
	});
})(jQuery);

Die Nutzung würde hierbei wie folgt aussehen:

function fn(){
	alert('F');
}
//bind live:
$('div.teaser-wrapper').addLive('div.teaser', 'click', fn);
// unbind live/die
$('div.teaser-wrapper').removeLive('div.teaser', 'click', fn);

Fazit:

jQuery 1.4 benötigt nicht unbedingt eine neue Methode für Event-Delegation. Die Verwendung des context-Parameters wird nun grundsätzlich ermöglicht und damit mögliche Fehler einer flaschen Benutzung minimiert. “Power User”, die jedoch Wert darauf legen, daß sie die volle Kontrolle darüber haben, welchem Element der Event-Listener genau hinzugefügt wird, würden sich jedoch stark über eine anders funktionierende Methode freuen.

Eine solche Methode wäre dann nicht nur einfacher zu erklären, sondern würde ebenfalls die Forderung nach einer performanteren (ohne Unnötiges Selektieren von Elementen, die man nicht braucht) berücksichtigen.

Written December 8, 2009 by
alexander farkas

Wai-Aria Widget-Entwicklung mit Accessibility Probe/Inspect am Beispiel einer custom Select-Drop-Down-Box

Tags: accessibility, javascript, jquery

Einleitung

Die Entwicklung von Aria-Widgets ist letztendlich keine triviale Sache. Zum einen müssen alle für das Widget wichtigen Aria-Attribute vorhanden sein, die Verschachtelung und letztendlich auch das – durch den Entwickler zu implementierende – Verhalten stimmen. Fehler in der HTML-Struktur oder dem Verhalten können häufig größeren Schaden anrichten als helfen. Bei der Entwicklung von zugänglichen Javascript-Komponenten erhält daher das Testen mit verschiedenen Screenreader-/Browser-Kombinationen eine hohe Bedeutung. Dieses Tutorial soll helfen zu zeigen, wie einfach man “bessere” Wai-Aria-Widgets schreibt bzw. wie man bereits vorhandene Widgets beurteilen kann, um den Entwickler auf Fehler aufmerksam zu machen.

Dies möchte ich am Beispiel einer normalen Ausklappliste demonstrieren, doch vorab hier eine Demo sowie ein Screencast, welches das fertige Aria-Widget mit verschiedenen Screenreader/Browser-Kombinationen zeigt.

Gute Resourcen

Die zentrale Anlaufstelle für gute Inhalte zum Thema WAI-Aria stellt Codetalks von Mozilla zur Verfügung. Dort lassen sich zahlreiche Aria-Beispiele/Test Cases, Artikel, FAQs, Tutorial, Blogs, Tools rund um das Thema Aria finden.

Einiges sei hier extra erwähnt.

  • Die entscheidenden Passagen des Aria Best Practices Dokuments sollten immer vor der Entwicklung eines Widgets gelesen werden. Das Dokument enthält sowohl allgemeine Informationen (z.B. Tastaturbenutzung/Fokus-Management) sowie konkrete Informationen zu entsprechenden Widgets (in unserem Fall der combobox).
  • Scripte sollten immer mit mehr als einem Screenreader und mehr als einem Browser getestet werden. Todd Kloots hat hierzu im YUI-Blog einige Informationen zum Installieren und Konfigurieren von Screenreadern für Entwickler zusammengestellt. Marco Zehe gibt weitere Informationen zum Testen mit NVDA.
  • Gute Tools machen uns das Entwicklen deutlich einfacher. Neben Tools wie Firebug/Dragonfly sind dies für die Aria-Entwicklung insbesondere die Firefox Accessibility Extension sowie eines der im Titel dieses Tutroials genannten Accessibility-Inspect Tools (Inspect bzw. Accessibility Probe).
  • Eine mögliche Anlaufstelle für Fragen sowie Diskussionen über eigene Lösungen/Ansätze stellt die Free ARIA Community dar.

Die Accessibility-Infomationen des select-Elements (im Firefox)

Die Aria-Rolle combobox beschreibt sowohl das Widget Ausklappliste (Dropdown-Liste), bei dem der Benutzer zwischen vorgegeben Werten wählen kann, als auch das Widget “kombiniertes Eingabefeld”, bei dem der Nutzer zusätzlich freie Texteingaben machen kann. Wir werden in diesem Tutorial das Inspect-Tool von Microsoft dafür nutzen, um die richtige HTML-Struktur inklusive der Aria-Attrbiute für eine einfache Ausklappliste zu ermitteln und zu testen.

Wenn wir ein Select-Element mit folgender HTML-Struktur schreiben,

<label for="select-element" id="label">Bezeichner</label>
<select id="select-element">
	<option>Option A</option>
	<option>Option B</option>
</select>

… erhalten wir folgende Accessibility-Informationen:

Das select-Element besitzt einen Namen (das label), einen aktuellen Wert, eine Rolle (Kombinationsfeld), verschiedene default-Zustände und eine unsichtbare Liste, welche ebenfalls mit dem label-Element verbunden ist.

Sofern wir die unsichtbare Liste weglassen, erreichen wir mit folgender HTML-Struktur die Weitergabe ähnlicher Accessibility-Informationen im Firefox:

<span id="label">Bezeichner</span>
<div role="combobox" aria-valuetext="Option A" aria-labelledby="label" tabindex="0">
	Option A
</div>

Die obige HTML-Struktur berücksichtigt jedoch nicht die fatale Kleinigkeit, daß der Internet Explorer 8 die valuetext-Eigenschaft nicht unterstützt. Letztendlich kann man Microsoft zumindest bei der combobox keinen Fehler vorwerfen, denn die Aria-Spezifikation für comboboxen sieht dies selbst nicht vor. Bei der Spezifikation hatte man anscheinend vor allem das Widget kombiniertes Eingabefeld im Kopf und die Realisierung der einfachen Drop-Down-Liste vergessen (reine Behauptung/Vermutung). Das Aria Best Practices Dokument geht dann zwar auf Ausklapplisten ein (verkürzte HTML-Struktur: [role=combobox][tabindex=-1] > [role=textbox][aria-readonly=true][tabindex=0]), aber dies würde zu einer HTML-Struktur führen, die in Screenreadern nicht funktioniert.

Mit Bugs gegen Bugs

Ein Problem bei der Entwicklung von Aria-Widgets ist die Tatsache, daß semantische Überreste von Elementen übrig bleiben, wenn man die Rolle mit Aria ändert. Ein typisches Beispiel hierfür ist ein Anchor-Element mit einem href-Attribut.

<a href="#bla" role="button">Button-Text</a>

Bei dieser Grundstruktur wird zwar aus einem Link ein Schalter, aber dieser Schalter besitzt neben dem Namen “Button-Text” noch zusätzlich den Wert der href-DOM-Eigenschaft als “Zugänglichkeitswert” (nicht zu Verwechseln mit dem Wert des href-Attributs). Aus diesem Grund sollte man, wenn man einen Link mit einer anderen Rolle belegt, das href-Attribut entfernen. Dies führt gleichzeitig dazu, daß

  1. der Link ohne tabindex-Attribut nicht mehr fokusierbar ist
  2. die Pseudoklasse :focus nicht in allen Browsern funktioniert (im IE6 funktioniert auch :hover nicht mehr)
  3. ein click-Event auf dem Link kein geräteunabhängiges Event mehr darstellt

Nun ist für uns die href-DOM-Eigenschaft nicht wirklich brauchbar, da wir diesen Wert nicht in der Hand haben. Das selbe Problem besteht jedoch ebenfalls bei Texteingabefeldern und läßt sich zur Lösung unseres Problems mißbrauchen.

Folgende HTML-Struktur ergibt sowohl im Firefox als auch im Internet Explorer die gewünschten Zugänglichkeitsinformationen:

<span id="label">Bezeichner</span>
<input role="combobox" value="Option A" aria-labelledby="label" tabindex="0" />

Die Ausklappliste selbst

Zusammen mit der Ausklappliste würde die HTML-Struktur wie folgt aussehen:

<span id="label">Bezeichner</span>
<input role="combobox" value="Option A" aria-labelledby="label" tabindex="0" />
<!-- weiteres HTML dazwischen | listbox wird ans Ende des Dokuments hinzugefügt -->
<ul role="listbox" aria-labelledby="label">
	<li role="option" tabindex="-1">Option A</li>
	<li role="option" tabindex="-1">Option B</li>
</ul>

Ändert der User mit der Maus oder mit den hoch/runter-Pfeiltasten die Optionen, wird dies über das zusätzliche Wai-Aria-Attribut activedescendant deutlich gemacht:

<span id="label">Bezeichner</span>
<input role="combobox" value="Option B" aria-labelledby="label" aria-activedescendant="option-2" tabindex="0" />
<ul role="listbox" aria-labelledby="label">
	<li role="option" id="option-1" tabindex="-1">Option A</li>
	<li role="option" id="option-2" tabindex="-1">Option B</li>
</ul>

Focus-Management: activedescendant vs. tabindex + focus

Grundsätzlich sollte man, statt der activedesendant-Methode, den Fokus mit tabindex und der focus-Methode ändern. Die focus-Methode funktioniert in recht vielen Browser-/Screenreader-Kombinationen, so daß Widgets, welche den focus auf diese Weise managen, selbst in Screenreader/Browser-Kombinationen funktionieren können, die kein Wai-Aria unterstüzten.

In unserem Fall, ist dieser Weg allerdings wenig sinnvoll, da wir die Liste ans Ende unseres Dokuments hinzufügen und dadurch aus der logischen Tabreihenfolge nehmen. Dem aufmerksamen Beobachter wird außerdem auffallen, daß wir den einzelnen Optionen dennoch ein tabindex-Attribut gegeben haben, obwohl wir die activedescendant-Methode nutzen und das Aria Best Practices Dokument in diesem Fall ein tabindex-Attribut für unnötig hält. Dies hat den Hintergrund, daß das tabindex-Attribut jedes Element nicht nur fokusierbar macht, sondern auch die Eigenschaft der Fokusierbarkeit (Markierbarkeit) an die jeweilige Zugänglichkeitsschnittstelle weiterleitet (Die Rolle option tut dies, wie fast alle Rollen, nicht implizit!). Fehlt dieses Attribut wird diese nicht weitergeleitet, was in einigen Screenreadern negative Folgen haben kann.

Wird die Liste reduziert, ist dieses Attribut wieder restlos zu entfernen.

Obgleich die Aria-Spezifikation noch weitere notwenige Aria-Attribute vorsieht (aria-expanded an der listbox) und weitere sinnvoll erscheinen (aria-checked an der jeweiligen Option, aria-selected=true|false an den Optionen), ist die oben gezeigte Aria-Struktur alles was wir für eine gut funktionierende Ausklappliste brauchen.

Endgültige HTML-Struktur

Mit dem Texteingabefeld als Workaround haben wir uns ein zusätzliches Problem geschaffen. Wird dieses fokusiert erscheint in der Regel ein Curosr und der Nutzer kann Text frei eingeben. Um dieses Verhalten rückgängig zu machen, müßen wir sowohl durch HTML-Struktur als auch unser Script einige Dinge berücksichtigen. Nachfolgend eine HTML-Struktur, welches einen Teil des ungewünschten Verhaltens rückgängig macht. Den Rest erledigen wir in unserem Script.

<span id="label">Bezeichner</span>
<div class="select" tabindex="-1">
	<input role="combobox" value="Option A" aria-valuetext="Option A" readonly="readonly" aria-readonly="false" aria-labelledby="label" tabindex="0" />
</div>
<ul role="listbox" aria-labelledby="label" id="datalist">
	<li role="option" id="option-1" tabindex="-1">Option A</li>
	<li role="option" id="option-2" tabindex="-1">Option B</li>
</ul>

Außerdem wollen wir die native Selectbox

  1. gegen eine schön stylbare/animierbare Selectbox tauschen
  2. in ihren Zugänglichkeitsinformationen nicht nur funktional, sondern möglichst originalgetreu nachbauen

und da brauchen wir bestimmt noch ein bißchen mehr HTML, so daß wir bei folgender Struktur landen:

<span id="label">Bezeichner</span>
<div class="select" tabindex="-1">
	<input role="combobox" value="Option A" aria-valuetext="Option A" readonly="readonly" aria-readonly="false" aria-expanded="false" aria-labelledby="label" tabindex="0" />
</div>
<div role="listbox" aria-labelledby="label" id="datalist">
	<div role="presentation">
		<ul role="presentation">
			<li role="option" id="option-1" tabindex="-1" aria-selected="true"><span role="presentation">Option A</span></li>
			<li role="option" id="option-2" tabindex="-1" aria-selected="false"><span role="presentation">Option B</span></li>
		</ul>
	</div>
</div>

Wann ist die Liste ausgeklappt/sichtbar?

Grundsätzlich klappt eine Auswahllliste für den User sichtbar erst bei gedrückter alt-Taste auf. Bei normaler Tastaturnutzung sieht es dagegen so aus, als würde sich lediglich der Wert des Select-Elements ändern. Wie ein Test mit dem Inspect-Tool zeigt, täuscht dieser Eindruck. Sobald der Wert mit den Pfeiltasten geändert wird, befindet sich der User automatisch auf der jeweiligen Option. Damit dies klappt, muß die Liste sichtbat gemacht (display: block; visibility: visible;) und das activedescendant-Attribut gesetzt werden. Es bleibt dem Autoren überlassen, ob er die Liste zu diesem Zeitpunkt außerhalb des Viewports plaziert oder nicht (In meiner Demo habe ich hierauf verzichtet).

Das Setzen des activedescendant-Attributs

Das activedescendant-Attribut sollte, ebenso wie das Setzen des Fokus mit der Fokus-Methode, immer mit einem timeout geschehen (Ein delay von 0 ms reicht). Wird das activedescendant-Attribut ohne delay auf ein Element gesetzt, welches zuvor noch versteckt war, kann der Screenreader dieses Element nicht finden und liest nichts vor. (Dies gilt nur für (neue) DOM-Elemente, nicht für die Änderung von Attributen oder Textknoten.)

Fazit

Es gibt viele Dinge, welche die Aria-Entwicklung erschweren: Browser-Bugs, Screenreader-Bugs, Merkwürdigkeiten in den Spezifikationen und letztendlich mangelndes Know-How. Geeignete Tools können uns helfen, diese Probleme schneller in den Griff zu kriegen. Ein Testen mit Screenreadern muß die Entwicklung begleiten.

Eine abschließende Frage an alle Entwickler, die mich immer wieder beschäftigt: Mit welchen Screenreader-/Browser-Kombinationen (inkl. Version) testet ihr bzw. bei welchen sollte – Eurer Meinung nach – die Funktionsfähigkeit sichergestellt werden (und sagt jetzt bitte nicht bei allen)?

Written September 23, 2009 by
alexander farkas

leichtgewichtiges, barrierearmes, generisches Hover-Plugin für jQuery

Tags: accessibility, deutsch, javascript, jquery, tutorial

Die hover-Methode von jQuery gehört wohl zu einer der beliebtesten Methoden von jQuery. Leider besitzt sie das Problem nicht barrierefrei zu sein, da die jeweils zugrunde liegenden Events (mouseenter/mouseleave) (eingabe-)geräteabhängig sind.

Die mit dem mouseenter/mouseleave häufig (aber nicht immer) korrespondierenden Events focus/blur werden dagegen selten eingesetzt. Andererseits gibt es einige fertige Scripte, die sich des Problems für bestimmte Teilbereiche angenommen haben. Sie setzen allerdings mehr oder weniger feste HTML-Strukturen voraus (In der Regel Flyout Menue Scripte mit verschachtelten Listen und darin befindlichen Links).

Ein Script, welches dieses – eigentlich einfache Problem – etwas generischer versucht zu lösen, ist mir nicht bekannt. Ich habe versucht dies vor einiger Zeit zu lösen. Hier eine Demo dieses Lösungsansatzes. Bei der Implementierung stößt man in der Regel auf mehrere Probleme:

  1. Das “Mausinteraktionselement” kann muß aber nicht immer das “Tastaturinteraktionselement”
  2. In einem “Mausinteraktionselement können mehrere “Tastaturinteraktionselemente” vorkommen
  3. Die Art der “Tastaturinteraktionselemente” ist nicht bekannt (jedes Element kann tastaturbenutzbar gemacht werden
  4. Wurde die Interaktion bereits durch die Maus getriggert, darf sie nicht nochmal durch ein focus getriggert werden und umgekehrt.

Kern der Lösung stellen hierbei die focusin/focusout Events dar, welche bubbelnde focus/blur Events darstellen und von Microsoft erfunden wurden. In standardkonformen Browsern läßt sich dieses Event dadurch nachstellen, daß man gefeuerte Events bereits in der Capturing-Phase abfängt.

Jörn Zaefferer hat für sein simples delegate-Plugin eine entsprechende focusin/focusout-Implementierung geschrieben, welche er auch in seinem bekannten Validation-Plugin nutzt. Jörn´s focusin/focusout Code sieht wie folgt aus:

/*
*	focusin/focusout von  jörn zaefferer´s delegate plugin
*	 Copyright (c) 2007 Jörn Zaefferer
*/
$.each({
	focus: 'focusin',
	blur: 'focusout'
}, function( original, fix ){
	$.event.special[fix] = {
		setup:function() {
			if ( $.browser.msie ) return false;
			this.addEventListener( original, $.event.special[fix].handler, true );
		},
		teardown:function() {
			if ( $.browser.msie ) return false;
			this.removeEventListener( original,
			$.event.special[fix].handler, true );
		},
		handler: function(e) {
			arguments[0] = $.event.fix(e);
			arguments[0].type = fix;
			return $.event.handle.apply(this, arguments);
		}
	};
});

Unser barrierearmes hover-Plugin soll identisch zum hover-Plugin aufgerufen werden können. Als kleinen Bonus kann man als dritten Parameter Optionen angeben:

$('#nav').find('li').inOut(enterHandler, leaveHandler);
//oder mit anderen Optionen
$('#nav').find('li').inOut(enterHandler, leaveHandler, {bothOut: true});

Hierauf aufbauend nun die Grundkonstruktion unseres jQuery-Plugins:

(function($){
	var handler = {},
		uID = new Date().getTime()
	;

	$.fn.inOut = function(enter, out, opts){

	};

	$.fn.inOut.defaults = {
		mouseDelay: 0,
		keyDelay: 1,
		bothOut: false,
		useEventTypes: 'both' // both || mouse || focus
	};
})(jQuery);

Wir definieren als erstes ein handler-Objekt hier werden wir unsere handler-Methoden einfügen, die dann entscheiden, ob und wann die übergebenen Callback-Funktionen aufgerufen werden. Danach erstellen wir eine ID. Die ID werden wir später benötigen, da wir stark mit der data-Methode von jQuery arbeiten, um die einzelnen Zustände an den Elementen zu speichern. Für den seltenen Fall, daß man das Plugin mehrfach auf das selbe DOM-Element anwenden will, werden wir diese ID mit jedem Durchlauf des Plugins verändern. Hiernach kommt das eigentliche Plugin-Grundgerüst sowie die Default-Einstellungen.

Mit mouseDelay kann entschieden werden nach welchem Delay die jeweilige mouseenter/mouseleave Funktion auferufen werden sollen. Wir können dies sehr gut für benutzerfreundliche Flyout-Menues verwenden. Mit keyDelay können wir einen entsprechenden Delay für Tastaturuser einbauen. Allerdings macht dies hier weniger Sinn. Mit dem booleschen Wert bothOut können wir bestimmen, ob die leave-Methode nur dann aufgerufen werden soll, wenn weder das jeweilige Interaktionselement fokusiert noch die Maus drüber liegt. Mit der Option ‘useEventTypes’ können wir entscheiden, ob hover tatsächlich mit focus/blur gleichgesetzt werden soll (both). Anderenfalls können wir das Handling nur auf focus/blur (focus) oder nur auf mouseenter/mouseleave (mouse) beschränken.

Nun zum Inhalt unseres jQuery Plugins:

//hochzählen unserer ID
uID++;
//Defaults mit Optionen auffüllen
opts = $.extend({}, $.fn.inOut.defaults, opts);
//Option 'useEventTypes' verarbeiten
var inEvents = 'mouseenter focusin',
	outEvents = 'mouseleave focusout';
if(opts.useEventTypes === 'mouse'){
	inEvents = 'mouseenter';
	outEvents = 'mouseleave';
} else if(opts.useEventTypes === 'focus'){
	inEvents = 'focusin';
	outEvents = 'focusout';
}
//Anzahl der enter Events auf 0 setzen
//Eventhandler mit Optionen, der ID und eigentlicher Callback-Funktion binden
return this
	.data('inEvents'+ uID, 0)
	.bind(inEvents, [enter, opts, uID], handler.enter)
	.bind(outEvents, [out, opts, uID], handler.out);

Nun müssen wir noch die enter-/out-Methoden definieren. Als 1. die enter-Methode (wenn die Maus drüberwandert oder ein Element fokusiert wird):

handler.enter = function(e){
// Extrahieren der callback-Funktion, der Einstellungen und der ID aus der data-Array
	var fn = e.data[0],
		o = e.data[1],
		ID = e.data[2],
//Zwischenspeichern des Kontextes (zeigt auf DOM-Element)
		elem = this,
//Hochzählen unseres 'inEvents'
		inEvents = $.data(elem, 'inEvents'+ ID) + 1
	;
//Möglichen timer clearen, um ausführen eines "delayten" out-Callbacks zu verhindern
	clearTimeout($.data(this, 'inOutTimer'+ ID));
//setzen unserer aktuellen inEvents
	$.data(elem, 'inEvents'+ ID, inEvents);
//starten unseres Timeouts und speichern unserer timeout-Referenz
	$.data(elem, 'inOutTimer'+ ID, setTimeout(function(){
//enterCallback nur ausführen, wenn sie noch nicht ausgeführt wurde
		if(!$.data(elem, 'inOutState'+ ID)){
//setzen unserers inOutStates
			$.data(elem, 'inOutState'+ ID, true);
//modifizieren unseres Events
			e.type = 'in';
//eigentliche Callback-Funktion ausführen:
			fn.call(elem, e);
		}
//länge des Delay ermitteln
	}, /focus/.test(e.type) ? o.keyDelay : o.mouseDelay));
};

Nun müssen wir noch die leave-Methode erstellen, welche wie folgt aussieht:

handler.out = function (e){
	var fn = e.data[0],
		o = e.data[1],
		ID = e.data[2],
		elem = this,
		//sicherstellen das inEvents-Wert mind. 0 beträgt
		inEvents = Math.max($.data(elem, 'inEvents'+ ID) - 1, 0)
	;

	clearTimeout($.data(this, 'inOutTimer'+ ID));
	$.data(elem, 'inEvents'+ ID, inEvents);
	$.data(elem, 'inOutTimer'+ ID, setTimeout(function(){
		if($.data(elem, 'inOutState'+ ID) &&
			//Verarbeiten der bothOut-Option
				(!o.bothOut || !inEvents)){
			$.data(elem, 'inOutState'+ ID, false);
			e.type = 'out';
			fn.call(elem, e);
		}
	}, /focus/.test(e.type) ? o.keyDelay : o.mouseDelay));
};

Wie sich zeigt sind die enter-/leave-Methoden praktischen identisch. Die Unterschiede im Überblick: Dort wo die enter-Methode einen Wert hochzählt (inEvents), zählt die leave-Methode eins runter und dort wo die enter-Methode den Wert auf true setzt, setzt die leave-Methode den Wert false (inOutState). Daneben wird ausschließlich in der leave-Methode die Option bothOut verarbeitet.

Aus diesen geringen Unterschieden läßt sich eine kleine Funktion bauen, welche uns beide Handler-Methoden dynamisch erstellt.

$.each({enter: [1, 'in', true], out: [-1, 'out', false]}, function(handle, params){
	handler[handle] = function(e){
		var fn = e.data[0],
			o = e.data[1],
			ID = e.data[2],
			elem = this,
			inEvents = Math.max($.data(elem, 'inEvents'+ ID) + params[0], 0)
		;

		clearTimeout($.data(this, 'inOutTimer'+ ID));
		$.data(elem, 'inEvents'+ ID, inEvents);
		$.data(elem, 'inOutTimer'+ ID, setTimeout(function(){
			if((params[2] != $.data(elem, 'inOutState'+ ID)) &&
					(params[2] || !o.bothOut || !inEvents)){
				$.data(elem, 'inOutState'+ ID, params[2]);
				e.type = params[1];
				fn.call(elem, e);
			}
		}, /focus/.test(e.type) ? o.keyDelay : o.mouseDelay));
	};
});

Abschließend nochmals das gesamte Script sowie die Demo:

(function($){
	var handler = {},
		uID = new Date().getTime()
	;
	$.each({enter: [1, 'in', true], out: [-1, 'out', false]}, function(handle, params){
		handler[handle] = function(e){
			var fn = e.data[0],
				o = e.data[1],
				ID = e.data[2],
				elem = this,
				inEvents = Math.max($.data(elem, 'inEvents'+ ID) + params[0], 0)
			;

			clearTimeout($.data(this, 'inOutTimer'+ ID));
			$.data(elem, 'inEvents'+ ID, inEvents);
			$.data(elem, 'inOutTimer'+ ID, setTimeout(function(){
				if((params[2] != $.data(elem, 'inOutState'+ ID)) &&
						(params[2] || !o.bothOut || !inEvents)){
					$.data(elem, 'inOutState'+ ID, params[2]);
					e.type = params[1];
					fn.call(elem, e);
				}
			}, /focus/.test(e.type) ? o.keyDelay : o.mouseDelay));
		};
	});

	$.fn.inOut = function(enter, out, opts){
		uID++;
		opts = $.extend({}, $.fn.inOut.defaults, opts);

		var inEvents = 'mouseenter focusin',
			outEvents = 'mouseleave focusout'
		;
		if(opts.useEventTypes === 'mouse'){
			inEvents = 'mouseenter';
			outEvents = 'mouseleave';
		} else if(opts.useEventTypes === 'focus'){
			inEvents = 'focusin';
			outEvents = 'focusout';
		}

		return this
				.data('inEvents'+ uID, 0)
				.bind(inEvents, [enter, opts, uID], handler.enter)
				.bind(outEvents, [out, opts, uID], handler.out);

	};

	$.fn.inOut.defaults = {
		mouseDelay: 0,
		keyDelay: 1,
		bothOut: false,
		useEventTypes: 'both' // both || mouse || focus
	};

})(jQuery);

Angesichts der Tatsache, daß dieses Script nicht nur die meisten 08/15 Menue-Scripte überflüßig macht, sondern eine wirklich universell einsetzbare Lösung für unser hover-Problem darstellt, ist es wirklich klein und leichtgewichtig geraten :-) .

Update: Hier findet ihr eine aktuelle Version des inOut/enterLeave-Scripts

Written March 29, 2009 by
alexander farkas

Animation-Ajax-Queue erstellen

Tags: ajax, deutsch, javascript, jquery, tutorial, video

Eine häufigere Aufgabe ist es bei Ajax Requests den alten Content durch eine Animation zu verstecken, dann den Content auszutauschen und diesen dann durch eine weitere Animation anzuzeigen.

Hierbei stellt sich das Problem, daß die genaue Dauer der Ajax-Response unbekannt ist. Ist die Response deutlich kürzer als die Verstecken-Animation, kann der Inhalt nicht ausgetauscht werden, da der User dann während dieser Animation plötzlich bereits den neuen Content sieht.

Um dieses Problem zu lösen, eignet sich jQuery´s queue Methode sehr gut, welche bereits genutzt wird, um Animationen in eine Warteschlange zu stellen. Um diesen Animationsqueue zu nutzen, muß der Ajax-Callback sich zu diesem Queue hinzufügen. Wird die Animation noch ausgeführt, wird dessen Ende abgewartet ansonsten wird die Callback-Funktion sofort ausgeführt.

Eine kleine Funktion, die aus einer Callback-Funktion eine sich in die Effekt-Warteschlange “anstellende” Funktion macht, könnte wie folgt aussehen:

$.createQueueCallback = function(jElm, fn, type){
	type = type ||
		'fx';
	return function(){
		var that = this,
			args = arguments;

		jElm = $(jElm);
		jElm.queue(type, function(){
			fn.apply(that, args);
			jElm.dequeue(type);
		});
	};
};

Dieser Methode wird als 1. Parameter ein jQuery-DOM-Object, ein DOM-Objekt selbst oder ein CSS-Selektor, als 2. Parameter die Callback-Funktion und als 3. optionaler Parameter der Name der Warteschlange übergeben. Die Benutzung könnte so aussehen:

function requestContent(){
	var live = $('#live')
		.hide(500);

	$.ajax({
		url: 'htmlsnippet.txt',
		success: $.createQueueCallback(live, function(data){
			live
				.html(data)
				.show(200);
		}),
		error: function(){
			//immer errors handeln
		}
	});
	return false;
}

Folgend findet ihr eine Ajax-FX-Queue-Demo als Download.

Written March 15, 2009 by
alexander farkas

jQuery´s step-Methode erweitern: synchrone Width-Animation

Tags: deutsch, javascript, jquery, tutorial

jQuery bietet viele Möglichkeiten der Erweiterung. Neben der relativ gut bekannten Möglichkeit das jQuery Prototype Objekt mit weiteren Methoden zu versorgen ($.fn ist ein alias für jQuery.prototype). Gibt es APIs für die Erweiterung von Events, Selektoren, easing, Ajax und der step-Methode. Letztere spielt -wie das easing – eine zentrale Rolle bei Animationen. Die bekannteste step-Erweiterung ist wohl das Color-Animation-Plugin, welches es erlaubt auch Farbwerte zu animieren.

Wir wollen eine sehr simple step-Erweiterung schreiben. Bei diesem Tutorial geht es weniger darum eine tolle Custom-Animation zu schreiben, sondern zu lernen, wie man schnell und einfach eine solche Erstellen könnte. Möchte man 2 oder mehrere unterschiedliche Elemente mit unterschiedlichen Werten synchron animieren, kann man in der Regel einfach diese Animationen gleichzeitig starten. Hierbei kommt in der Regel nichts 100%ig synchrones heraus, jedoch fällt dies dem User nicht auf.

In manchen Fällen benötigt man jedoch eine wirklich 100%ige Synchronisierung von Effekten. In diesen Fällen kann eine Erweiterung der step-Methode der mögliche Weg zum Ziel sein. Die step-Methode wird – wie der Name bereits aussagt – bei einer Animation ständig aufgerufen. Nachfolgendend findet ihr eine Demo.

Unsere Step-Methode werden wir syncWidth nennen, so dass sie bei einer Animierung der Eigenschaft syncWidth jedes mal aufgerufen wird:

$('div.box:first').animate({syncWidth: 400});

Damit unsere Erweiterung weiß, welche Elemente synchronisiert werden und wieviel Platz für diese Elemente zur Verfügung stehen soll, geben wir in den Optionen zwei weitere Eigenschaften an:

$('div.box:first')
	.animate({syncWidth: 400}, {syncElements: 'div.box', fullWidth: 800});

Das Grundgerüst unseres Step-Plugins sieht wie folgt aus:

$.fx.step.syncWidth = function(fx){
	console.log(fx);
};

Als 1. Parameter übergibt jQuery uns das fx-Objekt der jeweiligen Animation. Mit Firebug können wir die in diesem Objekt zur Verfügung stehenden Informationen anschauen. Der gesamte weitere Code findet innerhalb dieses Grundgerüst statt.

Am Anfang kümmern wir uns um die initialen Berechnungen, die nur einmal ganz am Anfang durchgeführt werden müssen. Damit wir auf diese Berechnungen auch bei den späteren Aufrufen zurückgreifen können, erweitern wir einfach das fx-Objekt.

//fx.state ist nur beim 1. Aufruf falsy
if (!fx.state) {
	var o = fx.options;
	//Startwerte der Animation berechnen
	fx.start = $(fx.elem).width();
	//Zugriff auf wichtige Eigenschaft verkürzen
	fx.fullWidth = o.fullWidth;

	//Array für die Startwerte der synchronisierten Elemente
	fx.syncStart = [];
	fx.syncElements = $(o.syncElements)
	// neues jQuery-Objekt mit allen synchronisieten Elementen, aber ohne das fx.elem, erstellen
		.map(function(i, elem){
			if(elem !== fx.elem){
				return elem;
			}
		})
		//Array mit Startwerten füllen
		.each(function(i){
			fx.syncStart.push($(this).width());
		});
	//Endwert für synchronisierte Elemente berechnen
	fx.syncEnd = (fx.fullWidth - fx.end) / fx.syncElements.length;
}

Nun kümmern wir uns letztlich darum die einzelnen CSS-Änderungen zu berechnen und bei den jeweiligen Elementen zu setzen.

//syncedWidth enthält die Gesamtbreite aller synchronisierten Elemente
var syncedWidth = 0;
fx.syncElements
	.each(function(i){
		//Berechnung der aktuellen Breite des synchronisierten Elements
		//fx.pos gibt die Position in der Animation zwischen 0 und 1 an
		//mit Math.round erreichen wir, daß nur Ganzzahlen herauskommen,
		//da einige Browser ein schlechtes Subpixel-Rendering haben
		var width = Math.round(fx.pos * (fx.syncEnd - fx.syncStart[i]) + fx.syncStart[i]);
		//addieren der Breite des einzelnen Elements
		syncedWidth += width;
		//Breite des synchronisierten Elements setzten
		this.style.width = width + fx.unit;
	});
//Breite des Hauptelements berechnen und setzten
fx.elem.style.width = fx.fullWidth - syncedWidth + fx.unit;

Das war´s dann schon. Hier nochmals abschließend die Demo. Mit etwas Kreativität (habe ich nicht) und noch mehr Mathematik (kann ich auch nicht) kann man noch einiges aus der step-Methode rausholen. Abschließend nochmal der gesamte Code:

$.fx.step.syncWidth = function(fx){
	if (!fx.state) {
		var o = fx.options;
		fx.start = $(fx.elem).width();
		fx.syncStart = [];
		fx.fullWidth = o.fullWidth;

		fx.syncElements = $(o.syncElements)
			.map(function(i, elem){
				if(elem !== fx.elem){
					return elem;
				}
			})
			.each(function(i){
				fx.syncStart.push($(this).width());
			});

		fx.syncEnd = (fx.fullWidth - fx.end) / fx.syncElements.length;
	}
	var syncedWidth = 0;
	fx.syncElements
		.each(function(i){
			var width = Math.round(fx.pos * (fx.syncEnd - fx.syncStart[i]) + fx.syncStart[i]);
			syncedWidth += width;
			this.style.width = width + fx.unit;
		});

	fx.elem.style.width = fx.fullWidth - syncedWidth + fx.unit;
};
Written January 25, 2009 by
alexander farkas

jQuery.conConflict

Tags: deutsch, javascript, jquery

jQuery.conConflict = function(extreme){
	var $ = jQuery;
	var dontOverwrite = ["getInterface","name","addEventListener","constructor","jQuery","location","window","document","navigator","netscape","XPCSafeJSObjectWrapper","XPCNativeWrapper","GeckoActiveXObject","Components","parent","length","setInterval","top","scrollbars","scrollX","scrollY","scrollTo","scrollBy","getSelection","scrollByLines","scrollByPages","sizeToContent","prompt","dump","setTimeout","clearTimeout","clearInterval","setResizable","captureEvents","releaseEvents","routeEvent","enableExternalCapture","disableExternalCapture","open","openDialog","frames","find","applicationCache","self","screen","history","content","menubar","toolbar","locationbar","personalbar","statusbar","directories","closed","crypto","pkcs11","controllers","opener","status","defaultStatus","innerWidth","innerHeight","outerWidth","outerHeight","screenX","screenY","pageXOffset","pageYOffset","scrollMaxX","scrollMaxY","fullScreen","alert","confirm","focus","blur","back","forward","home","stop","print","moveTo","moveBy","resizeTo","resizeBy","scroll","close","updateCommands","atob","btoa","frameElement","showModalDialog","postMessage","removeEventListener","dispatchEvent","getComputedStyle","sessionStorage","globalStorage"];
	function doIt(){
		try{
			for(var prop in window){
				if($.inArray(prop, dontOverwrite) == -1){
					window[prop] = $;
				}
			}
			window.onload = $;
			window.$ = $;
		}catch(e){}
	}

	function doItExtreme(){
		try{
			for(var prop in $.prototype){
				Object.prototype[prop] = $.prototype[prop];
			}
		}catch(e){}
	}
	doIt();
	if(extreme){
		doItExtreme();
		setInterval(doIt, 999);
		$(doIt);
		$(window).load(doIt);
	}
};
Written November 10, 2008 by
alexander farkas

jQuery UI Widgets erweitern: Am Beispiel der UI-Canvasmap (Teil II)

Tags: deutsch, javascript, jquery, tutorial

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.

Written October 5, 2008 by
alexander farkas

Tutorial: Barrierearme Checkbox und Radio Button Ersetzung mit jQuery UI und der UI-Reflecting- Technik

Tags: accessibility, deutsch, javascript, jquery, tutorial

Demo und Script-Download

Problem und Lösungsansatz oder was ist UI-State-Reflecting

Eine gerne durchgeführte Aufgabe ist es, die in manchen Browsern recht hässlichen Checkboxen und Radiobuttons durch schönere, dem Design angepasste Eingabeelemente zu ersetzen. (Auf mögliche Verschlimmbesserungen durch schlechtes UI-Design möchte ich hier nicht eingehen. Dies gut zu machen, ist eine Herausforderung für den Designer!)
Möchte man hierbei möglichst barrierearm arbeiten, müssen die Zustände und Rollen der Eingabeelemente nicht nur optisch, sondern auch semantisch deutlich gemacht werden (hierzu eignet sich beispielsweise WAI-Aria).
Zusätzlich muss die Tastaturnutzung der Rolle und seines Zustandes entsprechend implementiert werden (für Radio Buttons für Checkboxen).

Insbesondere für die Implementierung der Tastaturnutzung für Radiobuttons ist ein etwas höherer Scripting-Aufwand nötig und leider sind die meisten JavaScript Entwickler in diesem Punkt entweder faul oder ignorant.

Reflektieren statt Simulieren

Dabei ist dies mit dem UI-Reflecting Ansatz mehr als einfach zu implementieren. Ausgangspunkt von UI-Reflecting ist die Erkenntnis, dass bereits der „Fallback“ entweder die Rollen und Zusände vollständig/teilweise abdeckt oder zumindest einen Teil der Funktionalität. In unserem Fall wird sowohl Funktionalität als auch Rolle und Zustand vollständig durch das Fallback abgedeckt und es findet lediglich eine optische Ersetzung statt. Beim UI-Reflecting lässt man also das Fallback die ganze Arbeit machen und der Entwickler bemüht sich lediglich darum dies auf dem neuen User Interface abzubilden.

UI-Widget-Factory

Die UI-Widget-Factory ist Teil von jQuery UI (in der ui.core.js). Um die Funktionsweise der Factory zu verstehen, empfehle ich folgendes UI Widget Tutorial.
Der Grundaufbau unseres unseres Scripts, wird wie folgt aussehen:

$.widget('ui.checkBox', {

});
$.ui.checkBox.defaults = {
    focusClass: 'focus',
    checkedClass: 'checked',
    disabledClass: 'disabled',
    hideInput: true,
    radioElements: false
};

In das noch leere Object-Literal werden wir die im folgenden beschriebenen Methoden integrieren:

Die Init-Methode:

Als 1. sammeln wir alle label-Elemente, welche mit dem jeweiligen input-Element durch das for-Attribut in Beziehung stehen. Diese(s) label-Element(e) werden durch entsprechende Hintergundbilder später die Rolle und den Zustand des input-Elements deutlich machen. Solltet ihr hierfür aufgrund von Designgründen nicht, die bereits vorhanden label-Elemente nutzen, sondern ein eigenes Element für die Optik des input-Elements nutzen wollen, könnt ihr vor dem instanziieren des Checkbox-Objects einfach ein weiteres label-Element einfügen. Beispiel:

$('input:radio').each(function(){
	var jElm = $(this);
		jElm
			.after('<label class="visualRadio" for="'+jElm.attr(id)+'">')
			.checkBox();
});

In der Regel wird dies jedoch nicht nötig sein.

Sofern es sich um ein Radiobutton handelt, holen wir uns alle Radiobuttons der Gruppe über das name-Attribut. Hintergrund ist, dass Radiobuttons sofern sie ungecheckt werden kein Cross-Browser-taugliches Event werfen und wir dies manuell nachholen müssen.

Im Anschluss verschieben wir das Original-input-Element aus dem Viewport. Dies machen wir allerdings davon abhängig, ob der User sich im Usermode (auch Kontrastmodus genannt) befindet, wofür wir ein weiteres kleines Script einsetzen. Sollte das Script nicht verwendet werden, gehen wird davon aus, dass sich der User nicht im Usermodus befindet.

Hiernach rufen wir unsere – noch zu definierende – reflectUI-Methode auf welche die Zusätnde des input-Elements durch CSS-Klassen auf unser(e) label-Element(e) überträgt.

Im Anschluss fügen wir noch die entsprechenden Event-Handler hinzu, um die Zustandsänderungen dynamisch ändern zu können. Damit das this weiterhin auf unser instanziiertes Object zeigt, setzen wir die bind-Function-Methode ein.

Hiermit ist unsere init-Methode fertig:

init: function(){
	var that = this,
		opts = this.options;
	this.labels = $('label[for='+this.element.attr('id')+']');
	this.checkedStatus = false;

        this.radio = opts.radioElements ||
		(this.element.is(':radio')) ?
		$(document.getElementsByName(this.element.attr('name'))) :
		false;

	var usermode = ($.userMode && $.userMode.get) ?
		$.userMode.get() :
		false;

	if(!usermode && opts.hideInput){
		var css = ($('html').attr('dir') == 'rtl') ?
			{position: 'absolute', right: '-999em', width: '1px'} :
			{position: 'absolute', left: '-999em', width: '1px'};
		this.element.css(css)
			.bind('usermode', function(e, o){
				if(o.usermode){
					that.destroy.call(that, true);
				}
			});
	}
	this.reflectUI({type: 'initialReflect'});
	this.element
		.bind('click.checkBox', $.bind(this, this.reflectUI))
		.bind('focus.checkBox blur.checkBox', function(){
			that.labels.toggleClass(opts.focusClass);
		});
}

Die reflectUI-Methode

Diese Methode ist sozusagen der Kern unserers Scripts. Sie stellt lediglich sicher, dass die Zustände (disabled und vor allem checked) auf unser User-Interface übertragen werden.

Wird eine Veränderung zum vorherigen Zustand erkannt, ruft die Methode die propagate-Methode auf, welche in vielen jQuery-UI-Widgets vorkommt und sehr interessant ist:

reflectUI: function(elm, e){
	var checked = this.element.is(':checked'),
		oldChecked = this.checkedStatus,
		o = this.options;
	e = e ||
		elm;
	this.labels[(this.element.is(':disabled'))?
		'addClass' :
		'removeClass'](o.disabledClass);

        this.checkedStatus = checked;

        if (checked !== oldChecked) {
            this.labels.toggleClass(o.checkedClass);
            this.propagate('change', e);
        }
}

Die propagate-Methode

Die propagate-Methode triggert an dem jeweiligen input-Element ein custom-Event, welche durch entsprechende Event-Objekte sowie Übergabe der eigenen Objektreferenz seine Zustände nach aussen trägt. Dies ist eine sehr gute Möglichkeit, um andere Widgets mit dem checkBox-Widget zu verbinden, ohne lästige starke Abhängigkeiten zu schaffen oder in den Original-Code einzugreifen. Es lässt sich aber auch für profaneres Gebrauchen. So kann man beispielsweise die Veränderung des checked-Status dynamisch durch eine Animation kenntlich machen.
Sofern es sich bei dem input-Element, um einen Radiobutton handelt, wird für alle Radiobuttons zusätzlich die reflectUI-Methode aufgerufen.

propagate: function(n, e){
	if(this.radio){
		this.radio.checkBox('reflectUI', e);
	}
    this.element.triggerHandler("checkbox" + n, [e, {
        instance: this,
        options: this.options,
        checked: this.checkedStatus,
		labels: this.labels
    }]);
}

Die destroy-Methode

Viele jQuery-UI-Widgets enthalten eine eigene destroy-Methode, welche dazu dient, das Widget mehr oder weniger auszuschalten und den Original-Zustand herzustellen. Bei unserer destroy-Methode kann gewählt werden, ob nur das Original-input-Element wieder sichtbar sein soll. Die Events und Zustandsanzeigen am label-Element bleiben erhalten oder ob zusätzlich alle Event-Handler entfernt werden sollen.

destroy: function(onlyCss){
	this.element.css({position: '', left: '', right: '', width: ''});
		if(!onlyCss){
			var o = this.options;
			this.element
				.unbind('.checkBox');
			this.labels
				.removeClass(o.disabledClass+' '+o.checkedClass+' '+o.focusClass);
		}
}

Kleine Verfeinerungen

Hiermit ist das Script bereits fertig und einsatzbereit. Als kleine Verfeinerung unseres Scripts fügen wir noch ein paar weitere öffentliche Methoden hinzu, die uns in anderen Situation noch hilfreich sein könnten:

disable: function(){
	this.element[0].disabled = true;
	this.reflectUI({type: 'manuallyDisabled'});
},
enable: function(){
	this.element[0].disabled = false;
	this.reflectUI({type: 'manuallyenabled'});
},
toggle: function(){
	this.changeCheckStatus((this.element.is(':checked'))?false:true);
},
changeCheckStatus: function(status){
	this.element.attr({'checked': status});
	this.reflectUI({type: 'changeCheckStatus'});
}

Fazit:

Wie sich gezeigt hat, ist der Scripting-Aufwand im Vergleich zum Ergebnis (komplette Tastaturnutzung wie Original-Elemente, richtige Rollen und Zustände bei Screenreadern etc.) relativ gering gewesen. Der Grund: Wir haben an keiner Stelle eine Tastaturnutzung oder ähnliches Scripten müssen. All das besorgt der Browser für uns.
Demo und Script-Download

Written September 6, 2008 by
alexander farkas
« older posts