protofunc()

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
protofunc