protofunc()

jme – das erste HTML6 audio/video development kit

Tags: HTML 5, accessibility, deutsch, javascript, jquery, video

Es gibt zwei Dinge mit denen mein jMediaelement Projekt allein nicht beschrieben wäre: HTML5 audio / video einerseits und Player-Script andererseits. Die von jme genutzen HTML5 Media-Elemente sind nur eine Abspieltechnologie von insgesamt dreien. Würde man den HTML5 Teil weglassen und damit lediglich Flash und VLC als Abspieltechnik dienen, hätte jme weiterhin imense Vorteile für Frontendentwickler. Das liegt unter anderem daran, daß jme eben kein reines Player-Script ist.

jme besteht aus 2 Hauptkomponeten:

  1. einem leichtgewichtigem, kugelsicheren reinen embed-Teil
  2. der “Vollversion”, welche neben dem embed-Teil, auch die DOM-API und die Markup-API (Controls) enthält

Media-Elemente einbinden schnell und einfach wie ein Bild

In vielen Fällen, in denen ein Entwickler einen Videoplayer einbinden muß, ist kein spezielles Design und kein besonderes Feature verlangt. Es geht schlicht weg darum einen Player zum Abspieln von Videos/Audios zu haben.

Der reine embed-Teil von jme ist daher in einer separaten Version als leichtgewichtiger Helper eingebunden.

Der notwendige HTML-Code ist identisch mit dem HTML5 Code:

<video src="myvideo.webm" poster="mein-poster.jpg" controls="controls">
	<!-- Fallback Content -->
</video>

Damit dies auch ohne Bugs und mit älteren Browsern funktioniert, kopiert man einfach die mm.embed.min.js aus dem packages Ordner vom jMediaelement-Download zusammen mit der player.swf und – falls man youtube-videos einbinden will – der yt.swf in einen gemeinsamen Ordner.

Danach kann man bei DOM-Ready seine HTML5 Media-Elemente mit jmeEmbed registieren:

$(document).ready(function(){
	$('audio, video').jmeEmbed({showFallback: true});
});

Der embed Teil kümmert sich um die Erkennung von Browser- und Plugin-Features, deren Einbindung/Initialisierung, der Normalisierung der HTML5 Mediaelement Attribute sowie der hiermit zusammenhängenden Bugs. Ist die Vollversion von jme eingebunden, wird hierdurch auch die DOM-API initialisiert. Die Vorteile für den Entwickler:

  • er hat einen einfachen, intuitiven und semantischen Einbindecode
  • Sofern ein Video mit richtigem Container/Codec eingebunden wird, erhöht sich die Zugänglichkeit für bestimmte Endgeräte
  • der Entwickler ist nicht gezwungen mehrere Video-Formate anzubieten. Letztendlich können auch nicht HTML5-Formate wie zum Beispiel flv-Dateien oder Youtube-Seiten als Datengrundlage genommen werden.
  • Die jeweiligen Browser-, Plugin-Player rendern die Kontrolelemente automatisch, so daß keine weiteren HTML-/CSS-Anpassungen anfallen.

Kurz: Gegenüber der Art wie man derzeit Videos einbindet, kommt man hier mit jme schneller, semantischer und manchmal auch zugänglicher ans Ziel.

HTML ist die Schnittstelle zwischen JavaScript und CSS oder “divide et impera” – Teile und herrsche

Der zweite Teil ist keine reine JavaScript API, sondern gliedert sich in DOM-API und Markup-API auf. Dieser Aufbau ist der Erkenntnis geschuldet, daß JavaScript nur wenig HTML diktieren sollte und HTML unter anderem eine Schnittstelle zwischen einerseits JavaScript und CSS und andererseits zwischen Frontend und Backend darstellt.

Die Erstellung von funktionsfähigen Kontrollelementen ist Aufgabe der Markup-API. Features welche nicht über die Markup-API realisiert sind, können mit Hilfe der DOM-API erledigt werden.

HTML6 Control-Elemente = jme Markup-API/Kontrollklassen

Anstatt, daß durch einen JavaScript-Aufruf ein Haufen unsemantischer HTML-Code um den Player gepackt wird, besteht jme aus kleinen Bausteinen, die einfach zusammengesetzt werden können und so einen individuellen Player ermöglichen. Hierbei besteht als oberstes Prinzip, daß sich die verwendeten Bausteine flexibel in die HTML-/DOM-Welt integrieren laßen müßen.

Stell Dir vor HTML5 ist Schnee von gestern und das W3C würde mit HTML6 einige neue Elemente spezifizieren, welche als Kontrollelemente für das audio- und video-element dienen. Ein Abspiel-/-Pause Button könnte wie folgt aussehen:

<button type="play-pause">play / pause</button>

Das schöne daran, der Browser kümmert sich um Funktionalität und Zugänglichkeit und der Entwickler besitzt die Flexibilität die Kontrollelemente erstens frei zu platzieren und zweitens mit weiterem HTML anzureichen, um Sie dann beispielsweise per CSS besser zu stylen oder um ein gewünschtes Userinterface-Verhalten zu erreichen.

Damit der Browser weiß, mit welchem Mediaelement das jeweilige Kontrollelement verbunden ist, werden die Kontrollelemente gemeinsam mit dem Mediaelement in einem ebenfalls neuen Element mit dem Namen player gewrappt. Das Ergebnis sieht dann wie folgt aus:

<player>
	<video src="myvideo.webm"></video>
	<button type="play-pause">play / pause</button>
</player>

Die jme Kontrollelement-Bausteine funktionieren hierbei auf ähnliche Weise. Die neuen Kontrollelemente werden durch beliebige valide Elemente mit bestimmten HTML-Klassen symbolisiert und lediglich das Wrapper-Element “player” wird durch einen JavaScript Aufruf ersetzt:

<div class="player-wrapper">
	<video src="myvideo.webm" controls="controls"></video>
	<button class="play-pause">play / pause</button>
</div>

Das jQuery-Plugin ‘jmeControl’, welches auf dem wrapper-Element aufgerufen wird, registriert hierbei sozusagen die einzelnen Kontrolelemente und ruft bei Bedarf, die embed-Methode auf. Genau wie bei der jmeEmbed-Methode muß grundsätzlich keine weitere Konfiguration vorgenommen werden.

$('div.player-wrapper').jmeControl();

Werden die nativen Kontrollelemente nicht angezeigt, wird der Flashplayer – sofern nicht ausdrücklich anders konfiguriert – automatisch in den wmode=transparent Zustand versetzt, um auch schöne Overlay-Kontrollelemente oder Overlay-Logos über dem Video erstellen zu können.

Man kann selbstverständlich auch mehrere Kontrollelemente unterbringen:

<div class="player-wrapper">
	<video src="myvideo.webm" controls="controls"></video>
	<button class="play-pause">play / pause</button>
	<span class="current-time">--:--</span>
</div>

jme kümmert sich hierbei nicht nur um die Funktionalität und rendert die jeweils aktuelle Zeit in das span-Element und erstellt ein Play-Pause-Toggle Verhalten aus dem button-element, sondern auch um die Zugänglichkeit. Insbesondere dann, wenn zum einfacheren Stylen statt dem button-Element ein span-Element genommen wird, fügt jme die gewollte Semantik per WAI-ARIA ein. Folgendes geschriebenes HTML ….

<div class="player-wrapper">
	<video src="myvideo.webm" controls="controls"></video>
	<span class="play-pause">play / pause</span>
	<span class="current-time">--:--</span>
</div>

…wird durch jme in folgendes HTML umgewandelt:

<div class="player-wrapper">
	<video src="myvideo.webm"></video>
	<span role="button" tabindex="0" class="play-pause">play / pause</span>
	<span role="timer" class="current-time">--:--</span>
</div>

Neben allgemein üblichen Video/Audio Kontroll-Elementen bietet jme über Plugins auch Verhaltensklassen für eine Playliste und Captions an.

Die Tatsache, daß wir eine Markup-API verwenden, die nicht von JavaScript diktiert wird, heißt aber nicht, daß wir nun alle möglichen Kontroll-/Verhaltensklassen auswendig lernen und wieder und wieder ins HTML schreiben müßen. jme bietet im utils-Ordner das jmeEmbedControls-Plugin an, welches nichts anderes macht, als einen Haufen Kontrollelemente in das Element, auf dem es aufgerufen wurde, einzufügen, um dann hierauf die jmeControl-Methode auszuführen. Diese Methode ist sozusagen das Schnellstarter-Kit für jme. Er ist bewußt ausgelagert, um jedem Webentwickler eine projektspezifische Anpassung zu ermöglichen.

einheitliche DOM-API statt vereinheitlichte JavaScript-API

Die vereinheitlichte JavaScript-API ist bei jme bewußt eine DOM-API, da nur so sich Mediaelemente, sei es nun HTML5, Flash oder VLC, in den normalen Workflow eines Frontendentwicklers integrieren können.

Ein Beispiel: JW-Player for HTML5 ist das erste HTML5 Player-Script, welches eine dokumentierte API vorweisen kann. Und obwohl jQuery, also ein klassischer DOM-Wrapper, als JavaScript Grundlage gewählt wurde, wurde keine DOM-API implementiert. Um einen Player zu scripten, sieht der Code wie folgt aus:

$.jwplayer().play();

Laut Dokumentation wird nun immer auf dem ersten eingebundenen Mediaelement die Methode play aufgerufen, möchte man nun jedoch auf einem anderen Mediaelement Methoden ausführen, muß man bereits zum Zeitpunkt der Initialisierung die Referenz auf dieses Objekt speichern und sicherstellen, daß alle Module die hiermit interagieren müßen an diese Referenz kommen:

//holen der Referenz auf ein bestimmtes Video bei der Initialisierung
var player = $('#stage video').jwplayer({
	flashplayer:'/static/files/player.swf',
	skin:'/static/files/five.xml'
});

//initialisieren aller anderen videos
$('video').not('#stage video').jwplayer({
	flashplayer:'/static/files/player.swf',
	skin:'/static/files/five.xml'
});

//arbeiten mit der gespeicherten Referenz
//irgendwo ganz anders im script
player.play();

Mit einer DOM-API sieht die Sache dagegen anders aus, denn die API ist Teil des DOMs und kann wie jede DOM-API mit der jQuery-Selektorengine selektiert werden.:

$('#stage video').play();

Ebenso wie man bei anderen DOM-Elementen auf Events horcht, Methoden chained etc., funktioniert dies auch bei jme. Das video bzw. das audio Element bleiben hierbei immer das zentrale API Element, gleichgültig ob tatsächlich HTML5 oder eben ein Plugin zur Anzeige der Mediainahlte genutzt wird.

$('video').bind('ended', function(){
	$(this).attr('autoplay', true).loadSrc('my-video2.webm');
});

Kurz: nur so kann Scripten Spaß machen.

Fazit

Merkwürdig aber wahr: jme stellt in vielen Punkten einige Weltrekorde auf:

  1. jme ist das erste audio/video Script mit Fallback, welches eine dokumentierte DOM-API besitzt (das werden garantiert einige nachmachen, da es sooo nahe liegt)
  2. jme ist das erste audio/video Script, welches eine Markup-API für Kontroll- und Zustandselemente verwendet
  3. jme ist – und das ist wirklich traurig – das erste Script seiner Art, welches semantischen Code erlaubt/nutzt (notfalls mit WAI-ARIA forciert)

Oder anders gesagt: jme versucht nicht aus HTML5 Flash zu machen, sondern aus Flash HTML5.

Written June 6, 2010 by
protofunc

HTML 5 Mediaelemente für alle – Warum und Wie video-/audio- Elemente schon heute genutzt werden sollten

Tags: HTML 5, deutsch, javascript, jquery, video

HTML 5 Mediaelemente für alle – Warum und Wie video-/audio- Elemente schon heute genutzt werden sollten

Ich gehöre mit Sicherheit nicht zu denjenigen, welche die Nutzung von HTML5 in seiner reinen Form bereits heute propagieren. Die Probleme der Rückwärtskompatibilität mit HTML5-Elementen sind zu groß und können eben nicht mit dem document.createElement – Trick behoben werden. Ausnahmen bestätigen jedoch die Regel und eine hiervon sind mit Sicherheit die HTML5 Mediaelemente, audio und video. Warum sollten diese Elemente also zum Einbinden von Multimediainhalten genutzt werden, wenn immernoch Flash oder andere Plugins – zumindest als Fallback genutzt werden müßen?

audio/video Elemente sind einfacher zu Handhaben als object-/embed-Elemente

Im Laufe der Zeit haben sich eine Menge Einbindetechniken für Flash-Player entwicklet. Diese Menge an Einbindetechniken sind jedoch kein Zeichen von Flexibilität, sondern Ausdruck für die Unzulänglichkeiten jeder jeweils anderen Einbindetechnik. Ein paar Beispiele:

  • verschachtelte object-Elemente bzw. verschachtelte object-/embed – Elemente

    fehleranfällig, da unterschiedlicher Code für unterschiedliche Browser; kaum ein Entwickler kann sich den genauen Einbindecode merken; unelegantes HTML

  • Einbindung mit swfobject oder ähnlichem

    kann nicht ohne Javascript funktionieren; kaum ein Entwickler kann sich den Einbindecode merken; Herausforderungen dies elegant mit einem CMS zu lösen

  • Javascript transformiert ein HTML-Element automatisch zum Videoplayer-Code (z. B.: a.video -> object)

    kann nicht ohne Javascript funktionieren; angesichts der Vorhandenen HTML5-Semantik ist diese Einbindung semantisch überholt

Auf der anderen Seite stehen die HTML5-Elemente audio/video, welche die Einbindung von Mediainhalten so einfach machen, wie die Einbindung von Bildern.

	<video src="mein-video.mp4" poster="mein-poster-bild.jpg" controls="controls"></video>

Unterstützt der Browser nicht das video-Element bzw. das Video-Format kann dies mit Javascript erkannt und in ein unterstütztes object-Element umgewandelt werden. Was bleibt ist jedoch ein klarer Einbindecode, der semantisch und elegant ist und in modernen Browsern grundsätzlich auch ohne Javascript und vor allem ohne weitere Plugins funktioniert.

Es funktioniert auf mehr Endgeräten

Spätestens seit der unsäglichen iPhone/iPad – Flash Diskussion wissen alle, daß Flash nicht auf allen Geräten installiert ist. Bindet man Mediainhalte per HTML5 ein und fällt – bei nicht Unterstützung – auf ein oder besser mehrere mögliche Plugin(s) (Flash, Quicktime, VLC etc.) zurück, erreicht man schlichtweg mehr User.

jMediaelement – Video/Audio für alle

Das jMediaelement-Script auf Basis von jQuery stellt eine Lösung dar, welche erkennt, ob die HTML5-Mediaelemente im allgemeinen und die konkreten Mediadaten im besonderen unterstützt werden. Bei Bedarf bindet es unterstützte Plugins ein (derzeit Flash auf JW Player Basis sowie VLC), normalisiert die unterschiedlichen APIs und hilft bei der Erstellung von stylbaren Kontrollelementen für die Player.

Mediainhalte mit jMediaelement einbinden

Die Einbindung von Video- und Audio – Dateien mit jMediaelement ist dementsprechend sehr einfach. Als Grundlage muß man lediglich den HTML Code schreiben, wie er durch HTML 5 vorgesehen ist. Die Attribute poster, loop, autoplay, controls werden hierbei unterstützt. Man kann statt einer Source-Datei auch mehrere Dateien verwenden. Als mögliche Source-Dateien kommen neben MP3, MP4 und OGG auch FLV, MOV und weitere in Frage.

Nachfolgend ein paar Beispiele:

<video src="mein-video.mp4" poster="mein-poster-bild.jpg" controls="controls"></video>

<video loop="loop">
	<a class="source" href="mein-video.flv" controls="controls">Video als flv</a>
	<source src="mein-video.flv" />
</video>

<video poster="mein-bild.png" autoplay="autoplay" controls="controls">
	<a class="source" href="mein-video.ogg">Video als ogg</a>
	<source src="mein-video.ogg" />
	<a class="source" href="mein-video.mp4">Video alsmp4</a>
	<source src="mein-video.mp4" />
</video>

Anschließend ruft man einfach das jQuery Plugin auf den Audio/Video Elementen auf. In der Regel muß man hier nur den Pfad zur Flashplayerdatei konfigurieren:

$('audio, video').jmeEmbed({
    jwPlayer: {
        path: '../mediaplayers/player.swf'
    }
});

Zur “kugelsicheren” Einbindung mit schönem Fallback, falls alle Stricke reißen, kann noch etwas mehr getan werden, aber dies soll hier erstmal reichen.

Multimedia scripten war noch nie so einfach

Eine Sache die mit jMediaelement wirklich Spaß macht, ist das Scripten der Player. Zum einen ist die API für die unterschiedlichen Player normalisiert, zum anderen erweitern diese jQuery selbst. Gleichgültig welche API und welcher Browser verwendet wird, ist und bleibt das HTML5-Element hierbei das anzusprechende Element, so daß mit den Mediaplayern gearbeitet werden kann, wie man dies von jQuery gewohnt ist. Eine Auflistung der meisten Methoden findet sich im jMediaelement-Wiki.

Hier ein paar Beispiele:

Sind mehrere Video- und/oder Audio-Player auf einer Seite und sollen, sobald der User einen Player startet, alle anderen pausiert werden, könnte dies folgender einfacher Code bewerkstelligen:

$(document).bind('play', function(e){
	$('audio, video').not(e.target).pause();
});

Wir wollen Sprungmarken in das Video erlauben. Klickt der User auf eine Sprungmarke springt der Player an diese Stelle und fängt an zu spielen. Wir haben hierfür folgendes HTML vorbereitet:

<video class="skip-video" src="mein-video.mp4" poster="mein-poster-bild.jpg" controls="controls"></video>
<ul class="skip-links">
	<li><a href="#" data-time="10">jump to 10</a></li>
	<li><a href="#" data-time="20">jump to 20</a></li>
	<li><a href="#" data-time="30">jump to 30</a></li>
</ul>

Dann würde unser Javascript wie folgt aussehen:

$('ul.skip-links').delegate('a', 'click', function(e){
	var time 	= parseInt($(this).attr('data-time'), 10);
	if(isNaN(time)){return false;}
	$('video.skip-video')
		.currentTime(time)
		.play()
	;
	return false;
});

Wir wollen, daß ein Video automatisch, aber im gemuteten Zustand, abspielt. Zwar kennt der HTML5 Standard das Attribut autoplay, aber für den mute-Zustand gibt es keine Markup-API. Unser HTML könnte dann so aussehen:

<video class="automute" autoplay="autoplay" src="mein-video.mp4" poster="mein-poster-bild.jpg" controls="controls"></video>

… und unser Javascript wie folgt:

$('video.automute').muted(true);

Bisher haben wir die Player nur in verschiedene Zustände versetzt (currentTime, play, pause, muted). Es ist aber genauso möglich, Zustände auszulesen. Hierzu müßen wir allerdings abwarten, bis die API bereit ist, ausgelesen zu werden. Dies sagt uns die Methode jmeReady. Hier ein kleines Beispiel:

$('video.myvideo').jmeReady(function(){
	var muted = $(this).muted(); //true|false
	var time = $(this).currentTime(); //am Anfang wohl in der Regel 0
	//etc.
});

Manche Daten wie beispielsweise die Länge des Videos/Audios sind erst noch später verfügbar, nämlich dann, wenn die Metadaten geladen wurden, wofür das Event loadedmeta zur Verfügung steht. Hier ein Beispiel:

$('video.myvideo').bind('loadedmeta', function(){
	var duration = $(this).getDuration(); //das video dauert xxx Sekunden
});

Unter meinen jMediaelement-Demos befindet sich eine Audioplayer-Demo, welche zeigt, wie einfach mit der Methode loadSrc eine Playliste erstellt werden kann.

Stylebare Kontrollelemente erstellen

Die Erstellung von stylbaren HTML-Kontrollelementen wird durch jMediaelement ebenfalls unterstützt. Hierbei ging es mir um die größtmögliche Flexibilität für den Webautoren. Anstatt Kontrollelemente in einer mehr oder – in der Regel – weniger konfigurierbaren, lokalisierbaren und barrierearmen Form mit Javascript in das HTML zu Rendern, darf(/muß) der Entwickler sein HTML selber schreiben. Das einzige was vorgegeben wird, ist eine HTML-Klasse, welche dem Script sagt, wie sich das Kontrollelement verhalten sowie ein Mechanismus, welcher Player hierdurch gesteuert werden soll.

Wir wollen beispielsweise einen Play-/Pause-Togglebutton machen. Die vorgegebene Klasse nennt sich hierfür ‘play-pause’.

Unser HTML könnte dann wie folgt aussehen:

<a href="#" class="play-pause">play / pause</a>
<!-- oder -->
<button class="play-pause">play / pause</button>
<!-- oder, aber nicht empfohlen, da nicht tastaturbenutzbar -->
<span class="play-pause">play / pause</span>

Folgende Schreibweisen wären jQuery UI Themeroller kompatibel:

<a href="#" class="play-pause"><span class="ui-icon"></span> play / pause</a>
<!-- oder -->
<button class="play-pause"><span class="ui-icon"></span> play / pause</button>
<!-- oder, empfohlen: -->
<button class="play-pause"><span class="ui-icon"></span> <span class="button-text">play / pause</span></button>

Für die Assoziierung eines Kontrollelements mit einem bestimmten video-/audio-Element, biete ich im Prinzip 3 Möglichkeiten an:

  • gemeinsames wrapper-Element
  • data-controls-Attribut am Kontrollelement (HTML5-valide, data-controls=”IDREF”)
  • data-controls-Attribut an einem wrapper-Element

gemeinsames wrapper-Element

Wird die jQuery-Methode ‘jmeControl’ auf einem Wrapper-Element ohne data-controls-Attribut aufgerufen, werden alle darin befindlichen Kontrollelemente mit dem ersten gefundenen video/audio-Element assoziiert:

Unser HTML:

<div class="video-wrapper">
	<video src="mein-video.mp4" poster="mein-poster-bild.jpg"></video>
	<a href="#" class="play-pause">abspielen / pausieren</a>
	<a href="#" class="mute-unmute">ton aus / ton an</a>
</div>

Unser Javascript:

$('div.video-wrapper').jmeControl();

data-controls-Attribut am Kontrollelement

Soll das Kontrollelement eigentlich ganz woanders auf die Webseite, gibt es unter Umständen keinen gemeinsamen wrapper, welcher andere video/audio-Elemente ausschließt bzw. wäre obige Schreibweise ineffizient.

Unser HTML:

<div class="video-wrapper">
	<video id="my-video" src="mein-video.mp4" poster="mein-poster-bild.jpg"></video>
</div>
<!-- ganz viel HTML dazwischen -->
<a data-controls="my-video" href="#" class="play-pause">abspielen / pausieren</a>

… und unser Javascript:

$('a.play-pause').jmeControl();

data-controls-Attribut an einem wrapper-Element

Exsistieren mehrere Kontrollelemente weit vom zu kontrollierenden video-/audio-Tag entfernt, wäre es müßig jedem einzelnen Element das data-controls-Attribut zu geben.

Unser HTML:

<div class="video-wrapper">
	<video id="my-video" src="mein-video.mp4" poster="mein-poster-bild.jpg"></video>
</div>
<!-- ganz viel HTML dazwischen -->
<div class="control-wrapper" data-controls="my-video">
	<a href="#" class="play-pause">abspielen / pausieren</a>
	<a href="#" class="mute-unmute">ton aus / ton an</a>
</div>

… und unser Javascript:

$('div.control-wrapper').jmeControl();

Konfiguration der Kontrollelemente

Die Kontrollelemente sind daneben zusätzlich konfigurierbar. Beispielsweise sind alle slider sowie die progressbar Teil von jQuery UI und alle Optionen jQuery UI Optionen können verwendet werden. Wir wollen beispielsweise unsere Klassen Namespacen, da wir in einem ganz anderen Modul bereits die Klasse play-pause verwenden, außerdem wollen wir, daß der bereits abgespielte Teil auf der Timeline anders eingefärbt wird und sich der Sliderhandle animiert auf der Timeline bewegt.

Unser HTML:

<div class="video-wrapper">
	<video id="my-video" src="mein-video.mp4" poster="mein-poster-bild.jpg"></video>
</div>
<div class="control-wrapper" data-controls="my-video">
	<a href="#" class="jme-play-pause">abspielen / pausieren</a>
	<div class="jme-time-slider"></div>
</div>

… und unser Javascript:

$('div.control-wrapper').jmeControl({
	classPrefix: 'jme-',
	timeSlider: {
		range: 'min',
		animate: true
	}
});

Fazit: Eine API sie zu knechten

Als ich mit dem Projekt angefangen habe, stand die Auseinandersetzung mit den HTML5 Mediaelementen im Vordergrund. Als ich dem ganzen einen Sinn verleihen wollte, war für mich der erste Gedanke, so viel HTML 5 wie möglich so wenig Flash/Plugins wie nötig, möglich zu machen. Diese Sichtweise hat sich inwzischen enorm geändert. jMediaelement ist für mich ein Tool, welches es erlaubt Mediainhalte einerseits einfach und elegant einzubinden und gegebenefalls ebenso einfach zu scripten und zu stylen sowie andererseits Mediainhalte auch dort zugänglich zu machen, wo entsprechende Plugins (noch) nicht verfügbar sind. Aber wem mein erster Gedanke eher gefällt, kann jMediaelement auch für seine Zwecke nutzen :-) .

Written April 19, 2010 by
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
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

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
protofunc

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"><span role="presentation">Option A</span></li>
			<li role="option" id="option-2" tabindex="-1"><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
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.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
protofunc
« older posts