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

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
protofunc

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
protofunc

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
protofunc

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
protofunc

Die jQuery UI-Widget-Factory am Beispiel einer Canvas Map: Teil I

Tags: deutsch, javascript, jquery, tutorial

Wir müssen nicht mehr lange warten und jQuery UI 1.5 wird ist released. Neben einigen Komponenten und hübschen Effekten, bietet der Core eine – für Plugin-Autoren – interessante Widget-Factory, welche ich mal am Beispiel einer Canvas Imagemap näher unter die Lupe nehmen möchte. Als erstes sollte man sich daher die neuesten Versionen von jQuery, UI und explorercanvas (wir wollen ja, dass die Beispiele auch ein bisschen im IE funktionieren.) besorgen. Hier findet ihr eine – zugegebenermaßen – häßliche Demo des einfachen CanvasMap-Plugins (ich bin kein Grafiker und scheitere bereits bei Imagemaps :-) .

Die HTML Struktur und das CSS

Bevor wir anfangen solltet ihr einen kleinen Blick in das HTML werfen. Ihr werdet sehen, dass wir erstens ein Bild mit der Karte Berlins haben und zweitens ein transparentes Bild, welches die eigentliche ImageMap darstellt. Der Grund hierfür ist einfach. Canvas erzeugt anders als SVG richtige Pixelgrafixen. Man kann hier leider keine Events, wie beispielsweise mouseover gezielt abfangen. Wir brauchen daher eine darüber liegende Interaktionsebene für den User.

Was macht die Widget-Factory?

Im Prinzip macht die Widget-Factory nicht sehr viel, aber sie macht es geschickt. Sie erstellt aufgrund eines – von uns zu definierenden – „Prototypen-Objekts“ ein ordinäres jQuery-Plugin und eine JS-Klasse. Das jQuery-Plugin dient hierbei als Initialisierungs- und Zugriffs-“Controller“ für die definierte Klasse. Um das folgende Tutorial zu verstehen sollte man also Grundkenntnisse in JavaScript-OOP besitzen.

Der Grundaufbau eines jQuery Widgets könnte so aussehen:

//Namespace erstellen, falls noch nicht vorhanden
$.namespace = $. namespace || {};
//Widget erstellen
$.widget('namespace.pluginname',
	{
		instancemethodeA: function(){
		},
		instancemethodeB: function(){
		},
	}
);
// default Optionen für das Widget
$.namespace.pluginname.defaults = {
	defaultOptionA: 'Hello',
	defaultOptionB: ' World'
};

Die Widget-Factory erstellt uns nun die gewünschte Klasse und das jQuery-Plugin:
Mit folgendem Ausdruck können wir unsere Klasse instantiieren:

$('#map').pluginname({defaultOptionB: ' mein Freund'});

Innerhalb unserer Instanzmethoden können wir auf das #map-Element mit this.element (Es handelt sich hierbei um ein jQuery-Objekt von #map.) und auf die Optionen mit this.options zugreifen (d.h. also this.options.defaultOptionB enthält den String ‘ mein Freund’ und defaultOptionA ist weiterhin der default-Wert ‘Hello’).
Wenn wir unsererem Widget ganz zu Anfang noch einige Dinge mitgeben möchten wie z.B. CSS-Klassen setzen, Events-Binden, zusätzliche Elemente hinzufügen, können wir eine Instanzmethode namens init definieren. Diese wird automatisch vom Constructor unserer Klasse aus aufgerufen (Den Constructor selbst liefert die Widget-Factory.).

Startcode unserers Canavasmap-Widgets

Wir wollen später das unser Widget wie folgt aufgerufen werden kann:

$('#map').canvasmap();

Das #map-Element ist hierbei nicht die Imagemap selbst, sondern ein Container, welcher die Bilder und die Imagemap umfasst.

$.pf = $.pf || {};
$.widget("pf.canvasmap", {
	init: function(){
		var that = this;
		this.canvas = $(document.createElement('canvas'));
		this.img = $('img[usemap]:first', this.element[0]);
		var ieDiv = $('&lt;div class="canvas"&gt;&lt;div&gt;').insertBefore(this.img[0]);
		ieDiv.prepend(this.canvas[0]);
		if($.browser.msie && this.canvas[0] && !this.canvas[0].getContext && typeof G_vmlCanvasManager != 'undefined'){
			 this.canvas = $(G_vmlCanvasManager.initElement(this.canvas[0]));
		}
		if(this.canvas[0] && this.canvas[0].getContext){
			this.ctx = this.canvas[0].getContext('2d');
			this.areas = $('area', this.element[0]);

			if(this.options.clickBorderWidth || this.options.clickBG){
				this.img.bind('click.canvasImagemap', click);
				this.areas.bind('click.canvasImagemap', click);
			}

			if(this.options.overBorderWidth || this.options.overBG){
				this.img.bind('mouseover.canvasImagemap', over).bind('focus.canvasImagemap', over);
				this.areas.bind('focus.canvasImagemap', over).bind('mouseenter.canvasImagemap', over);
				this.img.bind('mouseout.canvasImagemap', out);
				this.areas.bind('blur.canvasImagemap', out).bind('mouseleave.canvasImagemap', out);
			}
		}
	}
});
$.pf.canvasmap.defaults = {
	overBG: 'rgba(0,185,227,0.5)',
	overBorderColor: 'rgba(255,255,255,0.5)',
	overBorderWidth: 1,
	clickBG: 'rgba(0,185,227,1)',
	clickBorderColor: 'rgba(255,255,255,1)',
	clickBorderWidth: 2
};

Als erstes erstellen wir unseren Namespace (pf für pfrotofunc) und definieren die 1. Methode sowie unsere Defaults für unser Plugin namens canvasmap. In der Initialiserungsmethode erstellen wir ein Canvas-Element fügen es in einen div-Container und platzieren es vor das Imagemapbild. Der Container ist für das später auszuführende explorercanvas notwendig. Als nächstes überprüfen wir, ob das Canvas-Malobjekt (this.canvas[0].getContext) zur Verfügung steht.
Wenn dem nicht so ist, haben wir es wahrscheinlich mit dem IE zu tun und rufen G_vmlCanvasManager.initElement auf dem Element auf, wandeln es in ein jQuery-Objekt und speichern es in unsere canvas Eigenschaft.

Nach einer weiteren „Sicherheitsabfrage“ speichern wir unser Malobjekt in this.ctx und binden unsere Events.

Da die Browser recht unterschiedlich mit Events bei Imagemaps umgehen, belegen wir sowohl das Bild als auch die einzelnen areas der Imagemap mit dem click und mouse-over/out – Event und da wir unobtrusiv und barrierearm arbeiten, stellen wir den Mausevents noch focus- und blur-Events zur Seite.

Der aufmerksame Betrachter wird feststellen, dass die Eventhandler, die gebindet wurden, noch gar nicht existieren. Hierzu kommen wir aber zum Schluss zurück, wenn der übrige Aufbau unserer Klasse feststeht.

Die weiteren Instanzmethoden

Die nachfolgenden Methoden werden – wie die init-Methode – ebenfalls im Canvas-Prototypen-Objekt gespeichert und mit der $.widget-Factory in eine Instanzmethode unserer $.pf.canvasmap-Klasse umgewandelt.

1. Die getArea-methode

Da wir – wie oben gesehen haben – unsere Events auch auf das Bild gelegt haben, aber wir natürlich mit der jeweiligen area-Arbeiten müssen (hier stehen nämlich die wesentlichen Informationen zum Malen drin.), müssen wir aus unserem Event die area extrahieren.

getArea: function(e, elm){
	var area = $(e.target);
	if(!area[0] || !area.is('area')){
		area = $(elm);
		if(!area[0] || !area.is('area')){
			area = false;
		}
	}
	return area;
}

Die getArea-Methode bekommt sowohl das Event-Objekt als auch das DOM-Element von unserem Eventhandler übergeben und muss nun entscheiden, welches von beidem die gewünschte Image-Map area enthält. Sofern sie keine area findet, ist irgendetwas schief gelaufen und sie gibt false zurück.

2. Die getCoords-Methode

Aus unserer Area müssen wir nun die einzelnen Koordinaten, welche im Attribut coords durch Komma separiert gespeichert sind, extrahieren. Daneben müssen wir das ganze noch in Zahlen umwandeln. Wir speichern das ganze dann in ein Array ab und geben es zurück.

getCoords: function(area){
	var coords = area.attr('coords').split(','), numCoords = [];
	for(var i = 0, len = coords.length; i < len; i++){
		numCoords[i] = parseInt($.trim(coords[i]), 10);
	}
	return numCoords;
}

3. Die drawArea-Methode

Nun sind wir kurz vor dem eigentlichen Mal-Vorgang. Wir benötigen für unser Beispiel nur den Shape-Typ ‘poly’, wir wollen aber später evtl. noch weitere Typen hinzufügen. Daher enthält unsere drawArea-Methode gar nicht den eigentlichen Mal-Code, sondern lediglich die Entscheidung, ob wir den Shape-Typen unterstützen und welche Mal-Methode aufgerufen werden soll:

drawArea: function(area, options){
	if(area){
		var shapeStyle = (area.attr('shape'))?area.attr('shape').toLowerCase():false;
		if(shapeStyle && this[shapeStyle]){
			var coords = this.getCoords(area);
			this[shapeStyle](coords, options);
		}
	}
}

Die Methode extrahiert also den Shape-Typ aus der Area, überprüft ob wir eine solche Methode definiert haben und wenn ja übergibt drawArea die Koordinaten sowie die Maloptionen an die eigentliche Mal-Methode.

4. Die poly-Methode

Nun malen wir aber wirklich:

poly: function(coord, o){
	this.ctx.beginPath();
	this.ctx.moveTo(coord[0],coord[1]);
	for(var i = 2, len = coord.length; i < len; i++){
		this.ctx.lineTo(coord[i],coord[i+1]);
		i++;
	}
	this.ctx.lineTo(coord[0],coord[1]);
	if(o.bg){
		this.ctx.fillStyle = o.bg;
		this.ctx.fill();
	}
	if(o.borderWidth){
		this.ctx.strokeStyle = o.borderColor;
		this.ctx.lineWidth = o.borderWidth;
		this.ctx.stroke();
	}
}

Als erstes sagen wir unserem Malobjekt, was wir malen wollen. Dann bewegen wir unseren Zeichenstift zur ersten Koordinate. Danach gehen wir das Array bei 2 beginnend, immer um zwei Schritte durch und ziehen unsere Linien. Sobald wir hiermit fertig sind, ziehen wir unsere Verbindungslinie zwischen End- und Anfangspunkt. Und zuletzt Füllen wir unsere gezeichneten Linien unseren übergebnenen Optionen (o) entsprechend aus und zeigen eine Border an.

5. Die drawClickArea-Methode

Um den Sinn der letzten Methode zu verstehen, muss man etwas zu canvas wissen. Es handelt sich wie gesagt um Pxielgrafiken. Man kann also nicht einfach sein Malobjekt verändern, sondern muss mit jedem Schritt das gesamte Bild löschen und von neuem anfangen. Gleichzeitig wollen wir aber, dass ein angeklickter Bereich auch dann noch gehighlightet wird, wenn man mit der Maus drüber fährt. Dafür speichern wir unseren geklickten Bereich in unserer Objektinstanz unter der Eigenschaft ‘ clickArea’ ab und rufen diese Methode beim hovern regelmäßig auf.

drawClickArea: function (){
	this.drawArea(this.clickArea, {
		bg: this.options.clickBG,
		borderColor: this.options.clickBorderColor,
		borderWidth: this.options.clickBorderWidth
	});
}

Nun haben wir alle Methoden zusammen und können uns den noch fehlenden Event-Handlern widmen:

function over(e){
	var area = that.getArea(e, this);
	that.ctx.clearRect(0,0,that.cWidth,that.cHeight);
	that.drawArea.call(that, area, {
		bg: that.options.overBG,
		borderColor: that.options.overBorderColor,
		borderWidth: that.options.overBorderWidth
	});
	that.drawClickArea.call(that);
	e.preventDefault();
	return false;
}
function out(e){
	that.ctx.clearRect(0,0,that.cWidth,that.cHeight);
	that.drawClickArea.call(that);
}
function click(e){
	var area = that.getArea(e, this);
	that.clickArea = area;
	that.ctx.clearRect(0,0,that.cWidth,that.cHeight);
	that.drawClickArea.call(that);
	e.preventDefault();
	return false;
}

Wie ihr seht löschen wir innerhalb dieser Funktionen das Canvas und rufen letzlich immer nur die bereits oben definierten Methoden auf.

Das Zwischen-Ergbenis unseres Canvasmap-Plugins.

Und was habe ich davon?

Dies werden sich nun einige fragen. Als 1. haben wir objekt-oriientierten Code hinterlassen. Dies hat in verschiedener Hinsicht Vorteile. So haben wir beispielsweise alle Zustände und Eigenschaften aus der DOM-Map sauber in unserer Objektinstanz zwischengespeichert. Die Eigenschaft areas zeigt beispielsweise nur auf die Areas unserer einen Imagemap, selbst wenn wir mehrere Imagemaps auf unserer Seite haben und diese mit unserem Plugin behandeln.

Einige jQuery-Plugin Autoren werden nun aufschreien und konstatieren, dass sie den selben Effekt mit der herkömmlichen Architektur ebenfalls hin bekommen. Dies stimmt tatsächlich. Je nachdem, ob man Funktion oder Variable außerhalb oder innerhalb der this.each-Funktion definiert, erreicht man einen derartigen „Speicherzustand“ entweder für alle DOM-Elemente des aktuellen jQuery-Objekts innerhalb seines Plugins oder für jedes einzelne DOM-Element innerhalb des Plugins.

Das hängt stark mit der Funktionsweise von Closures zusammen. Das Problem hierbei ist, dass eine Menge hilfreicher Funktionen und Variablen privat sind und schwer von außen zugänglich gemacht werden können.

Stellt euch beispielsweise mal vor euer Kunde kommt auf euch zu und will nun eine weitere Funktionalität implementiert haben. Ausserhalb der Imagemap soll eine Legende mit Kontrollelementen erscheinen. Sobald der User auf eines dieser Elemente klickt, soll ein bestimmter Stadtteil gehighlightet werden. Die gesamte Funktionalität habt ihr ja im Prinzip geschrieben.

Der Autor, welcher die herkömmliche Schreibweise von jQuery-Plugins benutzt hat, wird nun erklären, dass er für das Hinzufügen der neuen Funktionalität eine halbe Stunde benötigt. Daneben wird ihm – vorausgesetzt er steht auf sauber organisierten Code – der Kopf rauchen wie er diese Funktionalität sauber und flexibel in sein Plugin und möglichst als Option integriert.

Der Widget-Factory-Autor wird selbstverständlich selbiges behaupten, braucht aber beispielsweise keine Zeit sich zu überlegen wie er seinen Code nun neu organisieren muss und hat noch genügend Zeit für eine oder zwei Kippen :-) .

Zugriff von außen

Die UI-Factory Methode fügt unserem jeweiligen DOM-Element unsere Objektinstanz – ähnlich einem DOM-Expando (nur Memory Leak sicher) – hinzu. Wir haben daher auf unsere gesamte Objektinstanz mit folgendem Ausdruck Zugriff.

var instance = $.data($('#map')[0], 'canvasmap');

In der Regel werden wir dies nicht benötigen, da die Factory uns eine sehr schöne Möglichkeit bietet einzelne Methoden von aussen zu erreichen. Hierbei ruft man das normale jQuery-Plugin auf und übergibt als 1. Parameter den Namen der Instanzmethode. Alle weiteren Parameter werden ebenfalls als Parameter an die Methode weitergegeben:

$('#map').canvasmap('drawArea', $('#spandau'), {
	bg: 'rgba(200,100,100,0.5)',
	borderWidth: 1,
	borderColor: 'rgba(0,0,0,0.5)'
});

Dieser Code sagt also nichts anderes als: Mal die area mit der id ’spandau’ mit folgenden Optionen aus.

Einfacher geht es nicht oder? Demo für den Zugriff von außen.

Ausblick auf Teil II des Tutorials

Neben der Widget-Factory hat sich das UI-Team auch Gedanken darüber gemacht, wie die „Aussenwelt“ über entscheidende Zustandsänderungen innerhalb des Widgets informiert werden und wie man ein Widget modularisieren kann, um beispielsweise den Widget-Core klein zu halten oder um Dritten die Möglichkeit zu geben die Funktionalität zu erweitern/abzuändern, ohne dass eine Zeile des Original-Codes verändert werden muss. Wie man sein eigenes Widget derartig „pimpen“ kann soll der zweite Teil zeigen.

Written June 8, 2008 by
protofunc

Controlling Embedded Video with Javascript Part I: Quicktime

Tags: english, javascript, tutorial, video

This is the first part in a little series of articles about scripting the controls of video with Javascript. I worked on a project recently where using flash for video was not an option, but instead all other major formats/plugins had to be supported (quicktime, windows media player, real player, vlc player). While researching for this project, I noticed that there was a certain lack of easily accesible information about this topic, especially concerning cross-plugin and cross-browser functionality. That’s why I decided to write these articles, maybe they can help someone who’s trying to do something similar.

Javascript can be used to control nearly every aspect of the mentioned plugins’ behaviour. Usually the reason for scripting video plugins will be to create a custom control interface, while the standard controls are hidden. One should think very carefully about this from a usability perspective, because most users will be more familiar with the standard controls. Also there will always be certain (older) browsers-plugin-combinations that will not behave as expected, possibly preventing certain users from accessing the content. Therefore it’s always a good idea to have some sort of fallback plan (i.e. show standard controls if Javascript is deactivated… think graceful degradation).

This first article will cover the Quicktime plugin, followed shortly by WMP, Real, and VLC. Below is a list of desired features that should ideally work across all plugins and browsers. The approach I want to take is to create a common interface of functions covering the desired features and implement the interface for each plugin (view the interface functions – Note: This is an interface by convention only, it is not enforced through code). That way the underlying plugin can be easily swapped without changing much code. See the Quicktime scripting demo page for a working example.

List of desired features:

  1. cross-browser embedding with standard interface deactivated
  2. basic playback controls: play, pause, stop
  3. display current time, duration, and status information
  4. jump to specified time within movie
  5. volume control
  6. change/load movie
  7. buffering
  8. fullscreen display

Embedding Quicktime

To begin with we must embed the quicktime plugin properly so that it works in all major browsers (Quicktime version 7.4 as of writing). I won’t go into much detail here, since this has been covered sufficiently elsewhere (i.e. here or here). This is the method that I’ve used:

<object id="moviename"
    classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B"
    codebase="http://www.apple.com/qtactivex/qtplugin.cab"
    width="320" height="260">
    <param name="src" value="url/to/movie.mov" />
    <param name="controller" value="false" />
    <param name="autoplay" value="false" />
    <!--[if !IE]-->
    <EMBED name="moviename"
        height="260"
        width="320"
        src="url/to/movie.mov"
        type="video/quicktime"
        pluginspage="www.apple.com/quicktime/download"
        controller="false"
        autoplay="false"
    />
    <!--![endif]-->
</object>

In order to avoid the <embed> tag, which is non-standard (i.e. won’t validate), there are ways to use nested <object> tags instead (see A List Apart Articles: Bye Bye Embed), but I could not get the scripting to work properly in that case (any evidence of the contrary would be greatly appreciated…).

Note: This method of embedding the Quicktime plugin does not address the ActiveX activation issue in Internet Explorer – therefore you will currently need to click on the video once in order to activate it. It seems though that Microsoft is finally going to change the behaviour of embedded controls so that this won’t be an issue any longer after April 2008 (see: IEBlog: IE Automatic Component Activation (Changes to IE ActiveX Update)).

Basic Scripting

So let’s get started with the scripting, this is going to be pretty basic stuff – I might cover more advanced topics in separate articles. Note that the presented code is not fully optimized and I’ve ommited safety checks in order to preserve simplicity.

The first thing we need to do is to get a reference to the video object. The easiest way to do this, assuming the embed technique described above is being used, is to give the object tag an ID and the embed tag a NAME with identical values. That way the video can simply be accessed by its name:

var movie = document.moviename;

In this case Internet Explorer will use the ID attribute and all other browsers the NAME attribute. Once we have a reference to the actual video, we can start manipulating it – the basic playback controls would look something like this:

function playVideo() {
	movie.Play();
}

function pauseVideo() {
	movie.Stop();
}

function stopVideo() {
	movie.Stop();
	movie.Rewind();
}

Before actually manipulating the movie it is usually a good idea to check the status of the movie to make sure that it is ready for action. Otherwise if you try to access the video too early before it has loaded, Javascript errors can occur. The following method will return a string with the status of the movie:

function getStatus() {
	return movie.GetPluginStatus();
}

It’s safe to access the movie, if the status is either “Playable” or “Complete”. The returned string will have one of these values:
“Waiting” – waiting for the movie data stream to begin
“Loading” – data stream has begun, not able to play/display the movie yet
“Playable” – movie is playable, although not all data has been downloaded
“Complete” – all data has been downloaded
“Error: ” – the movie failed with the specified error number

The next two functions show how to access the current playback time and the duration of the movie. The thing to note here is that the time returned by “GetTime()” and “GetDuration()” are in the movie’s time scale units. In order to convert this to seconds one needs to divide it by the time scale (units per second). This will then give you the time in seconds, usually you will then want to convert this into a nicely readable string format (something like: hh:mm:ss).

function getTime() {
	return movie.GetTime() / movie.GetTimeScale();
}

function getDuration() {
	return movie.GetDuration() / movie.GetTimeScale();
}

In order to set the movie to a specific position, one basically has to do the opposite – multiply the movie’s timeScale with the desired position in seconds. Note that the time cannot be set beyond what has currently been buffered – if you attempt to do this it will set the time to the last buffered frame.

function setTime(sec) {
      movie.SetTime( sec * movie.GetTimeScale() );
}

The volume within Quicktime has a range of 0 – 255. I will convert this to be within a range of 0 – 100, because I think it is easier to work with in that range.

function getVolume() {
	return parseInt( movie.GetVolume() * 100 / 255 );
}

function setVolume(vol) {
	movie.SetVolume( vol * 255 / 100 );
}

To swap the movie that’s being played, just set a new URL. For some reason Quicktime will show the default controller again if you change the URL – so in order to keep it hidden, you will need to manually hide it again.

function loadURL(url) {
	movie.SetURL(url);
	movie.SetControllerVisible(false);
}

When a new movie is initially loaded it will be buffered into memory. It can be useful to give some feedback about the buffering status of a movie. The following function will return the percentage of the movie that has been buffered (0 – 100%):

function getBufferStatus() {
	var timeLoaded = movie.GetMaxTimeLoaded()
	var duration = movie.GetDuration();
	return parseInt( timeLoaded * 100 / duration );
}

As for full screen mode, it is currently (to my knowledge) not possible to invoke Quicktime’s internal full screen mode for embedded movies – even though since version 7.2 it has become a feature of the free standalone player.

That about wraps it up, check out the demo page and feel free to post any questions or feedback.

Links

Written February 1, 2008 by
boris