Fragen zu HTML5 und Co beantwortet 13 - Headlines, Main-Ersatz, DOM-Überwachung, Parent-Selektoren

Veröffentlicht am 22. April 2014

Leser fragen und der Erklärbär antwortet nun schon zum dreizehnten Male, heute zu den Themen HTML5-Semantik, DOM und CSS-Selektoren. Falls ihr hierzu oder zu anderen Browser-Themen noch mehr Fragen auf Lager habt, dann schreibt mir eine E-Mail oder gebt die Frage per Twitter ab.

Was passt in Headline-Elemente?

Ist es in HTML5 erlaubt, <p>-Elemente in den Headline-Elementen <h1> bis <h6> zu benutzen?

Nein, in Überschriften dürfen laut Spezifikationen nur Elemente aus der Kategorie phrasing content vorkommen. Das in etwa die Elemente, die man früher unter inline elements subsummiert hätte und so sind <p>-Elemente nicht mit von der Partie. Tipps für kompliziertere Überschrift-Strukturen hat dieser Artikel auf Lager. Am sinnvollsten ist es vermutlich, Überschrift und Absatz in einem gemeinsamen <header>-Element überzubringen.

Laut offizieller Empfehlung für komplexere Überschriften sollte man einfach ein Überschriften-Element zusammen mit z.B. <p>-Elementen in ein gemeinsames <header>-Element verfachten. Möglichweise wird es auch eines Tages ein Element der Marke <subhead> geben; erste Überlegungen hierzu existieren, aber definitiv ist hier noch gar nichts.

Herausfinden welches Element in welchem Element vorkommen darf ist im Übrigen ein Kinderspiel: einfach in den Spezifikationen das Element heraussuchen (hier die Headline-Elemente) und in der grünen (beim W3C blauen) Infobox die Punkte Content model und Contexts in which this element can be used ansehen; der erste verrät, was in diesem Element vorkommen darf, der zweite in welchen anderen Elementen dieses Element stehen kann.

Was ist der beste Ersatz für <main>?

Wenn ich <main> nicht benutzen kann, weil ich alte Browser unterstützen muss, wäre dann ein <section role="main"> oder ein <div role="main"> der richtigere Ersatz?

Ich halte es für etwas schwierig, hier das eine für falscher oder für richtiger zu erklären. Grundsätzlich gilt, dass man immer das passende Element verwenden sollte und nichts anderes. Die Spezifikationen sagen ganz eindeutig: Authors must not use elements, attributes, or attribute values for purposes other than their appropriate intended semantic purpose. So gesehen wären <div role="main"> und <section role="main"> beide nicht richtig, denn es gibt ja <main>. Daher mein Tipp: alten Browsern mittels html5shiv das neue Element einfach beibringen!

Wenn wir uns dann dazu entschließen (aus welchen Gründen auch immer) es falsch zu machen, sehe ich auch keinen so großen Unterschied zwischen <div> und <section> ausmachen. Für das <div> spricht, dass es genau wie <main> keinen Eintrag in der Outline erzeugt, so dass das vermutlich die bessere Wahl wäre. Aber man muss auch ganz ehrlich sagen, dass wir uns, wenn wir mit der Outline argumentieren, schon ganz weit draußen in der Theoriewüste bewegen. Für alles was relevant ist (SEO, Lesbarkeit des Quelltexts, Barrierefreiheit) ist es wirklich ziemlich egal, solange das role-Attribut da ist.

Es gilt wie immer die Faustregel für semantisches HTML: wenn man die Wahl zwischen A und B hat und nach 5 Minuten Nachdenken und Recherche nicht zweifelsfrei klar ist, was richtiger ist, ist es vermutlich ziemlich egal.

DOM-Änderungs-Events

Wie bekommt man mit, wenn sich ein Element im DOM ändert, es zum Beispiel sichtbar gemacht wird?

Für dieses Problem gibt es mehrere Lösungsansätze – die einen funktionieren in vielen Browsern und sind eher schlecht, die anderen sind eher gut, funktionieren bloß fast nirgends. Die beste Lösung ist der Mutation Observer (Specs, MDN), der einen Überwachungsprozess für einen DOM-Knoten darstellt. Mit new MutationObserver(callback) wird ein solcher Überwachungsprozess erstellt und mit der Methode observe(target, options) auf ein Ziel-Element angesetzt:

var target = document.querySelector('p');

// Neue MutationObserver-Instanz
// Das Callback-Argument "changes" ist ein Array von Änderungen,
// self ist der MutationObserver selbst
var observer = new MutationObserver(function(changes, self){
  console.log('Änderung registriert', changes);
  self.disconnect(); // Überwachung beenden
});

// Startet die Überwachung von "target" mit den im Objekt
// gelisteten Optionen. Hier interessieren wir uns nur für
// Änderungen des Class-Attributs
observer.observe(target, {
  attributes: true,
  attributeFilter: ['class']
});

MutationObserver können Inhalt, Attribute und Subtree eines Elements überwachen. Was genau überwacht werden soll, wird im zweiten Parameter der observe()-Methode angegeben. Wenn Änderungen passieren, feuert der beim erstellen des Observers angelegte Callback, wo die einzelnen Änderungen dem Array im ersten Parameter zu entnehmen sind. Hier ist das Ganze in Aktion

Was Mutation Observer nicht können, ist überall funktionieren – im IE gibt es sie z.B. erst ab Version 11 (siehe Can I use). Allerdings gibt es in vielen Browsern noch Fragmente der Mutation Events (MDN), die auf anderem Wege auch das Ziel der DOM-Überwachung im Visier hatten. Aus unter anderem Gründen der Performanceoptimierung (Observer sind viel schneller) wurden die Mutation Events allerdings relativ schnell offiziell aufgegeben, sind aber immerhin bis runter zum IE 9 noch vorhanden. Was tun, wenn man zwei verschiedene APIs hat die das Gleiche machen? Man nutzt einen Polyfill, der alle Browser und APIs vereinheitlicht, sofern sie denn überhaupt eine Möglichkeit zur DOM-Überwachung bieten. In IE 8 oder noch älteren Semestern ist DOM-Überwachung also nicht möglich, in allen anderen kriegt man das Problem zumindest auf eine einheitliche API zurechtgebogen.

Rückwärts matchende CSS-Selektoren?

Gibt es eine Möglichkeit per CSS alle Geschwisterelemente eines Elements anzusprechen? Mit dem General Sibling Combinator bekomme ich nur die nachfolgenden Geschwisterelemente, aber nicht die vorhergehenden.

Aktuell gibt es keine Möglichkeit, in CSS irgendwie rückwärts zu matchen, d.h. Elternelemente oder vorhergehende Geschwisterelemente anzusprechen. In Selectors Level 4 wird z.Z. ein für Parent-Selektoren benutzbares Feature beschrieben (E! > F spricht E an, wenn es das Elternelement von F ist), aber dieses Feature gibt es aktuell in keinem Browser und es ist auch alles andere sicher, dass sich das je ändern wird.

Das Grundproblem ist, dass CSS-Selektoren eigentlich eine Einbahnstraße sind. Browser lesen aus Performancegründen Selektoren wie .foo + section a immer von rechts nach links. In der Regel gibt es pro zu stylendem Element mehr CSS-Regeln nebst Selektoren, die das Element nicht betreffen als solche, die relevant sind. Um möglichst zügig entscheiden zu können, welche Styles der Browser auf ein Element anwenden muss, arbeitet er nach einem von rechts nach links laufendem Ausschlussverfahren.

Im Falle von .foo + section a beginnt er mit allen <a>-Elementen (was schon mal 95% aller übrigen Kandidaten aus der Suche eliminiert) und sortiert dann alle aus, die kein <section>-Element als Vorfahren haben. Am Ende fliegen dann <a>-Elemente, deren <section>-Vorfahren nicht direkt auf Elemente mit der Klasse foo vor sich haben aus der Auswahl und es bleiben nur die durch den Selektor beschriebenen Elemente übrig. Auf diese Weise wird der Löwenanteil der Element-Kandidaten im ersten Schritt (ist das Ziel-Element ein <a>?) aussortiert und der Rest-Selektor braucht gar nicht ausgewertet zu werden. Von links ausgewertete Selektoren würden es erfordern, dass große Teile des DOM gründlich nach möglichen Kandidaten durchsucht werden, also z.B. auch <section>-Elemente, die gar keine <a>-Elemente enthalten.

Bei einem Selektor für alle Geschwisterelemente ist das Performance-Problem vielleicht nicht ganz so groß, aber trotzdem müsste man der CSS-Engine beibringen, auch folgende Elemente in das Ausschlussverfahren mit einzubeziehen. Und das ist ein Feature, das es aktuell einfach aufgrund der fundamentalen Konzeption des Selektor-Parsers (Stichwort Bottom-Up-Parser) nicht gibt.

Dinge wie ein Parent-, oder Geschwister-Selektor würden den Matching-Algorithmus also um einiges komplizierter und vor allem langsamer machen. Der durch die Einführung eines Parent-Selektors entstehende Geschwindigekeitsnachteil wäre so groß, dass die Spezifikationen von Selectors Level 4 sogar unterschiedliche Performance-Profile definieren, von denen eins besonders schnell ist, aber eben Features wie den Parent-Selektor nicht unterstützt.

Müsste ich wetten, würde ich nicht darauf setzen, dass der Parent-Selektor oder ein Geschwisterelemente-Kombinator zeitnah in unseren Browsern aufschlägt.

Weitere Fragen?

Auch eure Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch als Erklärbär zu sich kommen lassen.

ECMAScript 6: Rest, Spread und Defaults

Veröffentlicht am 7. April 2014

Wenn ich Programmierern, die bisher mit Webentwicklung eher wenig am Hut hatten, JavaScript nahebringe, fange ich gerne mit den Bad Parts, den WTFs und den Wats an. Dann haben wir den unangenehmen Teil schnell hinter uns, ein paar Vorurteile werden bestätigt und Witze kann man damit auch schön reißen. Dass JS auch ein paar sehr feine Eigenschaften hat, kommt dann nach und nach von selbst zum Vorschein. Allerdings schickt sich ECMAScript 6 an, dieses Vorgehen in Zukunft zu erschweren, denn dort steht natürlich die Beseitigung (oder zumindest Abschwächung) der Bad Parts ganz oben auf der Agenda und eine dieser Maßnahmen zur WTF-Reduzierung sind Features rund um Funktionsparameter  da ist der Aufräumbedarf nämlich besonders hoch.

Fehlende Features und das Arguments-Problem

In JavaScript ist in Sachen „Bedienkomfort und Funktionsparameter“ viel Luft nach oben. So sieht in ECMAScript 5 der einzige Weg, Vorgabewerte für Funktionsparameter zu definieren, so aus:

var fn = function(x){
  if(typeof x === 'undefined'){
    x = 42;
  }
}

Das funktioniert einigermaßen, ist aber alles andere als komfortabel. Das nächste Problem zeigt sich, wenn man mit einer nach oben offenen Anzahl an Parametern zu arbeiten versucht. Innerhalb von Funktionen findet sich in JavaScript immer eine Variable namens arguments, die eine Liste der die Funktion beim Aufruf übergebenen Werte enthält und eigentlich mehr nach einem Feature als nach einem Problem aussieht:

var fn = function(){
  console.log(arguments);
};
fn('Hallo', 42)

// In der Konsole erscheint: ["Hallo", 42]

Die Sache hat aber einen Haken: arguments ist kein Array, sondern nur ein array-ähnliches Objekt. Spürbar wird der Unterschied vor allem darin, dass array-ähnlichen Objekten all die nützlichen Methoden fehlen, die Arrays so bieten: slice(), map(), forEach() und so weiter. Möchte man diese nutzen, so muss man arguments in ein echtes Array verwandeln, indem man es mit Array.prototype.slice() bearbeitet:

var fn = function(){
  var args = [].slice.call(arguments);
  return Array.isArray(args); // > true
};

Das ist zwar nur ein Einzeiler und für den geübten JavaScript-Entwickler nichts besonderes, aber trotzdem ist es schon recht lästig, dass man diese Umwandlung manuell vornehmen muss. Möchte man einen Schritt weiter gehen und nur einen Teil der Funktionsargumente als Array erhalten und den Rest weiterhin als normale Parameter verwenden, wird es endgültig unbequem:

var fn = function(a, b){
  var rest = (arguments.length >= 3) ? [].slice.call(arguments, 2) : [];
  console.log(rest);
};

fn(23, 42);             // > []
fn(23, 42, 1337, 9001); // > [1337, 9001]

Umgekehrt ist ist es auch nicht viel besser: hat man ein Array von Parametern für eine Funktion, so kommt man nicht um Function.prototype.apply() herum. Während das für sich genommen noch halbwegs unschlimm ist (abgesehen von der erforderlichen manuellen this-Festlegung), kann man, wenn man apply() benutzt, zusätzliche Werte ausschließlich durch Umbau des Arrays in die Funktion geben:

function fn(){
  console.log(arguments);
}

// Diese Args haben wir...
var args = [23, 42, 1337, 9001];

// ... aber vorher muss noch das hier dazu
args.unshift(2.718, 3.141);

fn.apply(undefined, args);
// > [2.718, 3.141, 23, 42, 1337, 9001] 

Eine Syntax für den Use Case „Funktionsaufruf mit A, B und allem was in diesem Array ist“ gibt es nicht, obwohl das ja nun wirklich keine abwegige Idee ist.

Die gute Nachricht ist, dass ECMAScript 6 all diese Probleme in Angriff nimmt. Es gibt eine Syntax für Funktionsparameter-Vorgabewerte und die neuen Rest- und Spread-Operatoren schicken arguments und Array-Gebastel in Rente. Wohlgemerkt: nur in Rente! Denn komplett abschaffen lassen sich die Bad Parts nicht …

Rest statt Arguments

Oberflächlich betrachtet wäre es ein schöner Fortschritt, wenn ECMAScript 6 arguments reparieren und in ein richtiges Array verwandeln würde. Das ist allerdings nicht möglich, da es da draußen im Web viel Code gibt, der fest davon ausgeht, dass arguments eben kein Array ist – und niemand möchte, dass plötzlich Scripts auf Webseiten nach einen Browserupdate einfach nicht mehr funktionieren. Also braucht es eine andere, arguments ergänzende Lösung, die in ES6 in Form von Rest-Parametern (ECMAScript-Wiki, MDN) daherkommt.

Rest-Parameter sind spezielle letzte Parameter in einer Funktionsdefinition, die mehrere an die Funktion übergebene Werte einfangen und in einem echten Array zur Verfügung stellen. Sie werden in der Funktionsdefinition mit drei vorangestellten Punkten markiert, tragen aber innerhalb der Funktion einen ganz normalen Variablennamen:

// Funktion mit zwei normalen Parametern
// Alle weiteren Parameter landen im Array "rest"
function fn(a, b, ...rest){
  return rest;
}

var rest = fn(23, 42, 1337, 9001); // > [1337, 9001]
Array.isArray(rest);               // > true

Für an CoffeeScript gewohnte Entwickler ist anzumerken, dass es nach aktuellem Stand nicht möglich sein wird, Rest-Parameter für etwas anderes als die letzten Parameter einer Funktion zu verwenden, so wie hier:

# Klappt, nur in CoffeeScript, nicht in ES6
fn = (x, rest..., y) -> rest
fn(23, 42, 1337, 9001) # > [42, 1337]

Die Vorteile von ...rest gegenüber arguments liegen auf der Hand: es ist ein echtes Array mit eingebauten Slice für die jeweils letzten an eine Funktion übergebenen Daten. Das arguments-Objekt wird aus Gründen der Abwärtskompatibilität bis ans Ende aller Tage ein Bestandteil von JavaScript bleiben, aber benutzen wird man es wesentlich seltener. Auch für Fälle, in denen man alle Funktionsparameter in einer Liste haben möchte, ...rest dank der vielen nützlichen Array-Methode vorzuziehen:

function add(...args){
  return args.reduce(function(a, b){
    return a + b;
  }, 0);
}

add(23, 42, 1337, 9001); // > 10402

Das heißt also im Endeffekt: arguments wird von ECMAScript 6 in den wohlverdienten Ruhestand geschickt.

Spread als Apply-Ergänzung

Möglicherweise ebenfalls aus CoffeeScript bekannt sind Spreads (ECMAScript-Wiki, MDN) mit denen sich eine Funktion mit einem Array von Parametern aufrufen lässt:

function sum(...args){
  return args.reduce(function(prev, curr){
    return prev + curr;
  }, 0);
}

var zahlen = [23, 42, 1337, 9001];
var summe = sum(...zahlen); // > 10402

Spreads vereinfachen nicht nur hier und da den Code, sondern ermöglichen auch Dinge, die mit ECMAScript 5 schichtweg nicht nur mit ausgesprochen mühsamen Tricks machbar waren. In ES5 gibt es Function.prototype.apply(), was verwendet werden kann um eine Funktion mit einer Liste von Werten als Arguments aufzurufen:

var summe = add.apply(null, [23, 42, 1337, 9001]);

Der Haken an apply() ist, dass man zwingend ein Objekt angeben muss, dass von der aufgerufenen Funktion als this verwendet werden soll. Das ist aber nicht immer möglich, z.B. bei Constructorfunktionen. Diese zeichnen sich ja gerade dadurch aus, dass sie ihr this-Objekt selbst erstellen und können daher nur mit new, nicht aber unter Zuhilfenahme apply() aufgerufen werden können. So weigert sich beispielsweise der Date-Constructor ohne new ein Objekt zu produzieren und spuckt stattdessen einen String aus:

var dateParams = [ 2014, 6 ];
new Date(dateParams[0], dateParams[1])); // > Objekt
Date.apply(null, dateParams));           // > String

Dabei ist Date noch ein relativ harmloses Beispiel – andere Funktionen würden mit apply() überhaupt nicht funktionieren und nicht mal ein falsches Ergebnis liefern. Mit Spreads löst sich der ganze Problemkomplex in Wohlgefallen auf, da es keinen Grund gibt, sie nicht mit Constructorfunktionen zu kombinieren:

var dateParams = [ 2014, 6 ];
new Date(dateParams[0], dateParams[1]); // > Objekt
new Date(...dateParams);                // > Objekt

Außer bei Funktionsaufrufen lassen sich Spreads auch für das Zusammenfügen von Arrays verwenden und ergänzen an dieser Stelle Array.prototype.concat():

var zahlen1 = [23, 42];
var zahlen2 = [1337, 9001, ...zahlen1]; // > [1337, 9001, 23, 42]

Es spricht auch nichts dagegen, die Spread-Syntax mehrfach in einem Funktionsaufruf oder einer Array-Erstellung zu verwenden:

var foo = [42, 1337];
var numbers = [23, ...foo, ...[9001]]; // > [23, 42, 1337, 9001]
sum(...[3.141], ...numbers);           // > 10406.141

Spreads sind einfach eine schöne kleine Ergänzung zur JavaScript-Syntax, die an vielen Stellen das vergleichsweise komplizierte apply() oder andere Hacks überflüssig macht.

Vorgabewerte für Parameter

Ein Feature, das fast jede Programmiersprache schon lange hat, landet mit ECMAScript 6 endlich auch in JavaScript:

function add(a, b = 42){
  return a + b;
}

add(23, 1337); // > 1360
add(23);       // > 65

Die Vorgabewerte werden erst zugewiesen, wenn die Funktion aufgerufen wird, was z.B. Funktionsaufrufe an der Stelle von Werten erlaubt. Im folgenden Beispiel wird für jeden add()-Aufruf auch getDefault() neu aufgerufen, was in unterschiedlichen Ausgabewerten resultiert:

function getDefault(){
  return (Math.random() > 0.5) ? 42 : 23;
};

function add(a, b = getDefault()){
  return a + b;
}

add(23); // > Manchmal 46, manchmal 65

Ein Vorgabewert wird immer dann von der Funktion benutzt, wenn der tatsächlich übergebene Wert undefined ist. Das hat zur Folge, dass man einen Vorgabewert auch durch ein explizites undefined nicht überschreiben kann:

function add(a, b = 42){
  return a + b;
}

add(23, undefined); // > 65

Es zeigt sich: die neue Syntax ist nichts weiter als ein 1:1-Ersatz des althergebrachten manuellen Wenn- Undefined-dann-X-Tests für Parameter, aber einfach wesentlich schöner und bequemer. Und das Beste ist: obwohl noch kaum ein Browser Vorgabewerte für Parameter unterstützt, kann man sie, wie auch Rest und Spread, bereits heute flächendeckend einsetzen.

Unterstützung und Ausblick

Der Firefox ist aktuell der alleinige Champion im Bereich Rest, Spread und Defaults. Rest- und Default-Parameter funktionieren seit Version 15, seit Nummer 15 auch Spreads für Arrays und seit 27 für Funktionen. Beim Rest der Browserwelt sieht es noch finster aus, wie uns die allmächtige Kompatibilitätstabelle wissen lässt. Allerdings werden alle drei neuen Features vom Traceur-Compiler perfekt unterstützt, so dass man sie im Prinzip ab sofort zum Einsatzbringen kann, vorausgesetzt der zusätzliche Kompilierschritt ist nicht zu abschreckend.

Erklärbär-Termine für April und Mai 2014

Veröffentlicht am 19. März 2014

Druckbetankung mit den neuesten Infos zu HTML5, CSS3, JavaScript und moderner Frontendentwicklung gefällig? Damit kann ich in den nächsten Wochen gleich mehrfach dienen:

  • 7. - 9. April in München: HTML5-Schulung bei der Open Source School. Mein bewährtes drei­tä­gi­ges HTML5-Standardprogramm stattet die Teilnehmer im Druckbetankungsverfahren mit so gut wie allem aus, was man zu HTML5 wissen muss. Von semantischem Markup bis hin zu Canvas-Frameworks ist alles dabei. Geboten wird ein großer Praxisanteil, kleine Arbeitsgruppen und ein Buch gibt es obendrein.
  • 10. und 11. April in München: CSS3 bei der Open Source School. Mein zweitä­gi­ges CSS3-Standardprogramm katapultiert die Teilnehmer in das CSS3-Zeitalter, in dem Webfonts und Farbverläufe fließen. Auch hier steht einen großer Praxisanteil mit überschaubaren Arbeitsgruppen auf dem Plan und mindestens ein CSS3-E-Book gibt es als Bonus.
  • 28. - 30. April in Düsseldorf: HTML5 MasterClass mit Jens Grochtdreis. Dreitägiges Intensivtraining zu HTML5, CSS3 und modernem JavaScript vom HTML5-Erklärbär und dem CSS-Buddha! Schritt für Schritt werden die neusten Webstandards vorgestellt, bis am Ende die Teilnehmer eine eigene kleine HTML5-App entwickeln. Early-Bird-Tickets noch bis zum 28. März!
  • 15. und 16. Mai in München: Online Print Symposium. Am Tutorial-Tag gibt es einen kleinen HTML5-Workshop, später dann einen kleinen, nichttechnischen Vortrag über das Hype-Thema HTML5.

Termine unpassend, Orte alle zu weit weg und Programme nicht genehm? Ich komme auch gerne mit einem maßgeschneiderten Programm vorbei – mich kann man mieten!

HTML5 Drag & Drop, Teil II: Drag & Drop für Elemente

Veröffentlicht am 17. März 2014

Dieser Artikel ist Teil einer Serie:

  1. Teil 1: Dateien
  2. Teil 2: Elemente
  3. Teil 3: Drop Effect

Die API, die HTML5 für Drag & Drop spezifiziert, gilt gemeinhin als ziemlich missraten. Die Schlimmheit fällt umso stärker aus, je komplexer der Use Case wird, was zur Folge hat, dass das in Teil 1 besprochene Drag & Drop für Dateien noch vergleichsweise simpel war. Auch das Ziehen von Elementen gestaltet sich nicht völlig katastrophal, ist aber ein wichtiger Schritt auf dem Weg in die wirklich üblen Untiefen der API, inklusive erster richtiger Internet-Explorer-Probleme.

Vorbereitungen

Unser Beispiel soll darin bestehen, dass es zwei Dropzonen gibt, in die der Nutzer via Drag & Drop andere Elemente hineinziehen können soll. Dabei soll jedes ziehbare Element nur in eine bestimmte Dropzone hineinpassen. Wie im ersten Teil können uns ganz normale <div>-Elemente als Container dienen, die auch die gleichen Styles wie bisher erhalten:

<div id="Markup" class="dropzone">Markup</div>
<div id="Style" class="dropzone">Style</div>
.dropzone {
  text-align: center;
  background: #EEE;
  border: 0.2em solid #000;
  padding: 3em;
  margin: 0.5em 0;
}

.dropzone.valid {
  background: #EFE;
  border: 0.2em solid #0F0;
}

Als ziehbare Elemente nehmen wir normale <li> mit etwas CSS:

<ul>
  <li>HTML5</li>
  <li>CSS3</li>
  <li>XHTML 1</li>
  <li>JavaScript</li>
</ul>
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
li {
  background: #EEE;
  border: 0.2em solid #000;
  padding: 0.5em;
  margin: 0.5em 0;
}

Außerdem können wir unserem Datei-Drop-Beispiel noch einige Zeilen JavaScript übernehmen. Das Lösen der Drop-Handbremse durch die dragover- und dragenter-Event sowie die Vergabe der Klassen brauchen wir auch diesmal:

$('.dropzone').on('dragover', function(evt){
  evt.preventDefault();
});

$('.dropzone').on('dragenter', function(){
  $(this).addClass('valid');
  evt.preventDefault();
});

$('.dropzone').on('dragleave', function(){
  $(this).removeClass('valid');
});

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  $(this).removeClass('valid');
});

Soweit, so gut (siehe jsFiddle).

Elemente ziehbar machen

Wie in Teil 1 erwähnt, wurde die HTML5-API aus dem IE5 reverse engineered. Dort, sowie auch in allen anderen Browsern, die sie zuvor implementiert hatten, diente Sie zum Abwickeln aller Drag & Drop-Operationen – also auch jener, die ganz ohne JavaScript zustande kommen. Markiert man etwas Text und zieht ihn durch die Gegend oder zieht man ein <a> irgendwo hin, so ist die exakt gleiche Logik wie in der HTML5-API am Werke. Das kann man sehr schön sehen, indem man in der Fiddle einfach irgendwelchen Text (z.B. ein paar Buchstaben aus den Labels der ziehbaren Elemente) markiert und ihn auf eine der Dropzonen zieht; die Zonen leuchten grün auf, weil auch die nativen Drag-Operationen von der API verarbeitet werden können.

Um auch andere Elemente neben <a> ziehbar zu machen, verpasst man ihnen das Attribut draggable="true":

<ul>
  <li draggable="true">HTML5</li>
  <li draggable="true">CSS3</li>
  <li draggable="true">XHTML 1</li>
  <li draggable="true">JavaScript</li>
</ul>

Dieses Attribut wurde im Zuge der HTML5-Entwicklung spontan erfunden, um überhaupt irgendwas anderes als Links und Bilder ziehbar machen. Das draggable-Attribut kann auch genutzt werden, um <a>-Elementen die Drag-Funktionalität entziehen, indem man es auf false setzt. Um das Ganze noch schön zu machen, bietet es sich an, den ziehbaren Elementen auch einen passenden Mauscursor zu verpassen:

[draggable=true] {
  cursor: move;
}

Die Drag-Operation kann durch drei Events via JavaScript verfolgt werden: dragstart feuert beim Start einer Drag-Operation auf dem gezogenen Element, drag findet laufend während einer Operation statt und dragend feuert, wenn eine Drag-Operation endet. Also noch fix ein wenig JS eingebaut …

$('li').on('dragstart', function(evt){
  console.log('Start');
});

$('li').on('drag', function(evt){
  console.log('Drag');
});

$('li').on('dragend', function(evt){
  console.log('Ende');
});

und schon funktioniert das Ziehen von Elementen! Jedenfalls funktioniert es (Stand Anfang 2014) in Chrome; der Firefox weigert sich, Drag & Drop ohne Daten durchzuführen, was (wenn auch durch die Spezifikationen nicht abgedeckt) verständlich ist; im Moment ziehen wir nur Elemente durch die Gegend, ohne dass wir irgendwelche sinnvollen Informationen übermitteln würden. Außerdem können wir aktuell jedes ziehbare Element in jede Dropzone ziehen. Das kann so nicht bleiben – wir brauchen Daten für unsere Drag-Operation.

Daten in Drag & Drop

Eins der Probleme mit Drag & Drop-Daten ist, dass die HTML5-Spezifikationen zwei APIs beschreiben: das gruselige Original aus dem alten IE und eine nicht ganz so gruselige Variante, die bisher nur teilweise in aktuellen Browsern zu finden ist. Das Grundprinzip ist jeweils gleich: beim Start einer Drag-Operation (beim dragstart-Event) schreibt man die zu übertragenden Daten das Event-Objekt. Der Browser merkt sich diese Daten und stellt sie am Ende einer Drag-Operation wieder in einem Event-Objekt zur Verfügung, so dass sich herausfinden lässt, welche Informationen genau durch die Gegend gezogen wurden. Um unser Script in mehr als einem Browser lauffähig zu bekommen, müssen wir uns Anno 2014 noch mit der alten Grusel-API abfinden. Und auf den ersten Blick scheint es auch gar nicht so schlimm zu sein.

Das Datenspeicher-Objekt heißt dataTransfer und ist ein Unterobjekt des Event-Objekts; in den Fiddle-Beispielen ist es dank jQuery unter evt.originalEvent.dataTransfer zu finden. Das Datenspeicher-Objekt hat die Methoden setData(type, data) und getData(type), die zum Setzen und Lesen von Daten genutzt werden können. Das Ganze funktioniert wie ein simpler Key-Value-Speicher für Strings. Auf unser Beispiel aufbauend könnten wir nun beim dragstart-Event Daten setzen und sie beim drop-Event wieder auslesen:

$('li').on('dragstart', function(evt){
  evt.originalEvent.dataTransfer.setData('text', 'Hallo Welt!');
});

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  window.alert(evt.originalEvent.dataTransfer.getData('text'));
});

Und schon funktioniert‘s sogar im Firefox! Allerdings funktioniert es auch nur auf exakt diese Art und Weise, denn der Datenspeicher ist nicht bei jedem Drag & Drop-Event les- und beschreibbar. Laut Spezifikationen bestehen Schreibrechte ausschließlich beim dragstart-Event und Leserechte nur beim drop-Event. Während aller anderen Event-Phasen kann weder gelesen noch geschrieben werden. Das klingt im ersten Moment sinnvoll, hat aber einen Haken: ob ein Ziel-Element ein valides Drop-Ziel ist, bestimmen wir im dragover-Event durch ein preventDefault() – das schon besprochene Lösen der eingebauten Handbremse. Im Moment können wir jedes <li>-Element auf jedes Ziel-<div> ziehen, auch das CSS3-Item in die Markup-Box. Um das zu unterbinden, müssten wir bei dragover in die Daten hineinschauen können, was so richtig nicht geht. Aber immerhin halb! Und nur außerhalb der modernen Internet Explorer. Aber immerhin!

Gezieltes Drag & Drop

In den Event-Phasen neben dragstart und drop kommt man an die Daten im dataTransfer-Objekt nicht heran, wohl aber an die Keys unter denen sie gespeichert sind – und die Spezifikationen sehen vor, dass man den Key frei wählen kann. Also vergeben wir doch einfach je nach Kategorie des gezogenen Elements unterschiedliche Keys. Mit Data-Attributen nehmen wir im Markup die Kategorisierung vor …

<div data-accept="markup" id="Markup" class="dropzone">Markup</div>
<div data-accept="style" id="Style" class="dropzone">Style</div>

<ul>
  <li data-type="markup" draggable="true">HTML5</li>
  <li data-type="style" draggable="true">CSS3</li>
  <li data-type="markup" draggable="true">XHTML 1</li>
  <li data-type="script" draggable="true">JavaScript</li>
</ul>
 … und speichern Daten mit dem Wert des data-type-Attributs der gezogenen Elemente als Key:
$('li').on('dragstart', function(evt){
  console.log('Start');
  var type = $(this).attr('data-type');
  var data = $(this).text();
  evt.originalEvent.dataTransfer.setData(type, data);
});

Die Handbremse zielgerichtet zu lösen scheint auch beinahe einfach zu sein. Im dragover-Event, wo dies passieren müsste, haben wir zwar keinen Zugriff auf den Inhalt von dataTransfer, können aber unter dataTransfer.types die Keys abfragen. Ein Problem hieran ist, dass einige Browser diese Key-Liste (standardkonform) als Array anbieten, andere (ehemals standardkonform) als DOMStringList. Arrays haben eine indexOf()-Methode, die DOMStringLists fehlt, während DOMStringLists eine contains()-Methode haben, die Arrays fehlt. Sobald man diese Differenz irgendwie unter Kontrolle gebracht hat, scheint der restliche Weg ganz leicht zu sein:

  1. Bei dragstart Daten unter einem Key setzen, der dem Wert des data-type-Attributs des gezogenen Elements entspricht
  2. Bei dragover schauen, ob Daten unter einem Key vorhanden sind, der dem Wert des data-accept-Attributs des Ziel-Elements entspricht; wenn ja, mit preventDefault() die Handbremse lösen
  3. Bei drop, das nur stattfindet wenn vorher in dragover ein preventDefault() passiert ist, die eigentlichen Daten auslesen

Den Code für Schritt 1 haben wir breits gesehen. Schritt 2, das dragover-Event, ist auch ganz simpel wenn man erst mal ein gemeinsames indexOf() DOMStringLists und Arrays gebaut hat:

// Universelles indexOf() für DOMStringLists, Arrays und mehr
var indexOf = Function.prototype.call.bind(Array.prototype.indexOf);

$('.dropzone').on('dragover', function(evt){
  var accept = $(this).attr('data-accept');
  // Entspricht ein Key unserem data-accept-Attribut?
  if(indexOf(evt.originalEvent.dataTransfer.types, accept) !== -1){
    evt.preventDefault(); // Drop erlauben!
  }
});

Am Ende ist es ein leichtes, mittels dataTransfer.getData() die übertragenen Daten auszulesen und anzuzeigen:

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  $(this).removeClass('valid');
  var key = $(evt.target).attr('data-accept');
  var val = evt.originalEvent.dataTransfer.getData(key);
  window.alert(val + ' ist ' + key);
});

Und schon klappt es … jedenfalls in Chrome, Opera, Safari und Firefox. In modernen Internet Explorern funktioniert das Script so nicht und eigentlich ist es auch noch nicht so ganz richtig rund.

Lücken, Macken und Internet Explorer

Nur weil Drag & Drop für HTML5 aus dem Internet Explorer reverse engineered wurde, funktioniert es dort noch lange nicht. Grundsätzlich wird die API natürlich schon unterstützt, nur unser Key-Differenzierungs-Trick funktioniert nicht. Die modernen IE akzeptieren ausschließlich text und url als Keys bei dataTransfer.setData() – und das, obwohl der Key-Trick sogar im offiziellen Spezifikations-Tutorial benutzt wird. Das ist etwas unglücklich, aber keine Vollkatastrophe; das einzige, was im IE definitiv nicht geht, ist die Unterscheidung zwischen verschiedenen Sorten von Element-Drag-Operationen. Wenn man nur eine solche Operation auf einer Seite hat (z.B. eine einzige umsortierbare Liste oder eine einzige Dropzone für Dateien) gibt es kein Problem.

Interessanter ist da schon ein noch fehlendes Feature unserer Beispiel-Fiddle. Zwar können wir Elemente von A nach B ziehen und auch sicherstellen, dass A nur in B und nicht in C abgeladen werden kann aber eins können wir nicht: A darüber informieren, dass es nach B gezogen wurde. Möglicherweise möchte man das Element A aus dem DOM verschwinden lassen wenn es irgendwohin gezogen wurde oder in ein anderes Element einsortieren. Das Problem ist, dass wir erst nach dem drop-Event wissen, ob eine Drag-Operation mit einem erfolgreichen Drop geendet ist– das drop-Event feuert allerdings auf dem Ziel-Element B, nicht auf dem gezogenen Element A. Zwar gibt auch das dragend-Event, das wie gewünscht auf A feuert, das allerdings immer tut  auch wenn es gar kein Drop gab. Die gute Nachricht ist: mit ein bisschen Mißbrauch alter IE-Features und der Kunst des indirekten Nachweises ist das Problem in den Griff zu bekommen. Wie genau das geht sehen wir im nächsten Teil dieser Serie.