protofunc()

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
protofunc