Web Components erklärt, Teil 1: Was sind Web Components?

Bei der Aufnahme der letzen Folge Working Draft (Revision 173) ging es um die neuen Features in Chrome 36, der aktuell im Beta-Stadium ist. Die Neuerungen sind vor allem für Web Components relevant … aber was sind „Web Components“ eigentlich? Was machen Sie, kann man sie schon benutzen und wie verhalten sich sich zu Projekten wie Polymer und X-Tag? Zeit das alles mal aufzuschreiben.

Welches Problem lösen Web Components?

Es gibt heutzutage kein Plugin-System für Frontend-Entwicklung. Es gibt allerlei Plugins für verschiedene Dinge, aber ein übergreifendes System steckt nicht dahinter. Einer Webseite eine neue Funktion beizubringen ist eine recht unangenehme Aufgabe, da es so viele verschiedene Wege gibt und all diese Wege irgendwelche Knackpunkte haben. Kommt die neue Funktion als jQuery-Plugin daher, braucht man jQuery. Wird sie in Form eines AMD-Moduls geliefert, muss man auch hierfür eine Library zur Hand haben und sich obendrein einen Wolf konfigurieren. NPM-Module müssen durch Browserify geheizt werden, „normale“ Libraries rümpeln irgendwelche Variablen in den globalen Scope (wenn man richtig Glück hat ist auch CSS dabei) und egal welchen Weg man beschreitet: am Ende muss man sich mit einer idiosynkratischen JavaScript-API herumschlagen. So baut man zum Beispiel eine Google-Map ein:

<script src="http://maps.googleapis.com/maps/api/js?key=APIKEY&amp;sensor=true">
</script>

<script>
  new google.maps.Map(document.getElementById('Map'), {
    center: new google.maps.LatLng(-52.033, 8.533),
    zoom: 8,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  });
</script>

Man darf also sowohl etwas HTML als auch zwei Scripts manuell irgendwie in die Seite bringen und man muss dafür sorgen, dass das JavaScript zum richtigen Zeitpunkt ausgeführt wird. Und das schlimmste ist: dieses Vorgehen und diese API funktionieren so nur für Google-Maps, auf andere Plugins kann man sein Wissen nicht anwenden. Ein ziehbares Element mit jQuery UI erstellt man völlig anders:

$('#draggable').draggable({ 
  containment: "parent" 
});

Die API ist einerseits völlig anders, allerdings hat man auch hier das Vergnügen, die Abhängigkeiten und das Element selbst einzubauen, die JavaScript-Ausführung zu timen und das Ganze auf eine Art und Weise mit CSS zu versehen, die hoffentlich weder im Rest der Webseite noch in dem Plugin selbst etwas falsch macht.

Mit Web Components verändert sich der Plugin-Einbau so, dass die Google-Map der Zukunft so eingebunden werden wird:

<link rel="import" href="google-map-plugin.html">

<google-map
  latitude="-52.033"
  longitude="8.533"
  zoom="8"
  type="roadmap"></google-map>

Das Plugin ist ein selbstdefiniertes HTML-Element, das über ein <link rel="import"> in die Ziel-Seite geladen wird. Das Element enthält intern allerlei Markup, CSS und JavaScript, das vom Rest der Seite isoliert ist – externes CSS kann nicht ohne weiteres das interne Markup verunstalten und internes CSS kann die Außenwelt nicht beeinflussen. Die Konfiguration des Plugins erfolgt vor allem über HTML-Attribute, aber auch JavaScript-Methoden und DOM-Events können bereitgestellt werden.

Web Components haben gegenüber dem heutigen Plugin-Wirrwarr eine Reihe von Vorteilen:

  1. Sie sind einheitlich, denn alles ist ein HTML-Element
  2. Sie sind einfach zu benutzten, denn jeder kann schon HTML. Selbst für technisch nicht sonderlich bewanderte CMS-Redakteure sollten Web Components zu meistern sein.
  3. Sie sind kombinierbar, denn sie sind ja nur HTML-Elemente. Eine Konstruktion wie <light-box><google-map></google-map></light-box> funktioniert auch ohne dass die beiden Elemente aufeinander abgestimmt sein müssen, so wie auch <div><p></p></div> einfach funktioniert
  4. Sie kapseln ihren inneren Aufbau (HTML, CSS, JS) und verhindern so Konflikte mit anderen Plugins oder Scripts auf der Webseite.

Web Components lösen also das Problem, dass Frontend-Entwicklung kein Plugin-System hat. Das Webseiten-Plugin der Zukunft ist ein selbstdefiniertes HTML-Element mit komplexem, aber von der Außenwelt sauber abgekapseltem Innenleben, das sich nach außen verhält wie jedes andere Element auch. Damit all das aber in jedem Browser funktioniert, muss eine ganze Menge zusammenkommen …

Was konkret sind Web Components?

Ähnlich wie „HTML5“ keine konkrete Technologie, sondern ein Dachbegriff für viele verschiedene Technologien ist, gibt es auch keine direkte „Web-Component-API“ im Browser. Stattdessen erlaubt ein Set von neuen APIs das Erstellen von Web Components, doch diese APIs können auch für viele andere Dinge verwendet werden. Die wichtigsten APIs aus dem Web-Component-Universum sind die folgenden:

Custom Elements

Wer eigene HTML-Elemente definieren möchte, braucht dafür die entsprechende Browser-Schnittstelle, die von der Custom Elements Specification beschrieben wird. Vom Prinzip her ist das Ganze kein Hexenwerk; man meldet sein neues Element einfach per document.registerElement() an und schon klappt es:

document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    // ...
  })
});

Das hier erstellte Element <x-foo> ist in dieser Form nicht mehr als ein etwas anders benanntes <div> ohne besondere Fähigkeiten. Den neuen Elementen spezielle Funktionen beizubringen ist schon etwas komplizierter, geht aber natürlich auch. Aber Stichwort Name: selbstgebaute Elemente müssen im Unterschied zu normalen Elementen einen Bindestrich im Namen haben (z.B. <google-map> oder <x-widget>), genießen aber sonst ziemlich die gleichen Rechte wie normale, eingebaute HTML-Elemente.

HTML Templates

HTML Templates sind genau das, was der Name vermuten lässt: Templates. Das Markup in einem <template>-Element wird vom Browser verarbeitet, aber nicht angezeigt – nur über eine JavaScript-API kann man den Inhalt (ein gebrauchsfertiges DocumentFragment) auslesen, eventuell vorhandene Platzhalter ersetzen und das Endergebnis dann irgendwo einfügen:

<template>
  <p>Ich bin Eintrag Nr. {{num}}</p>
</template>

<script>
  var tpl = document.querySelector('template').content;
  for(var i = 0; i < 3; i++){
    var p = tpl.querySelector('p').cloneNode(true);
    p.innerHTML = p.innerHTML.replace('{{num}}', i + 1);
    document.body.appendChild(p);
  }
</script>

Mit HTML Templates gibt es einen bequemen Weg, unsichtbar Markup für die Verwendung in Scripts überall in einer Webseite (oder einer Komponente) unterzubringen.

Shadow DOM

Das Shadow DOM eines Elements ist eine Art Sub-DOM-Baum in einem Element, der nicht für normale Scripts und andere Einflüsse zugänglich ist – der Sub-Baum versteckt sich eben im Schatten. Nimmt man es ganz genau, so ist der Shadow-Baum nicht wirklich ein Sub-Baum des Elements, sondern steht komplett außerhalb des normalen DOM und wird nur mit dem Element verbunden.

Ein bekanntes Beispiel für Shadow DOM sind die Bausteine des <progress>-Elements. In den meisten Browsern ist der Fortschrittsbalken über normales HTML umgesetzt, aber da es sich eben um Shadow DOM handelt, sieht man von diesem Unterbau nichts, wenn man das Element inspiziert. Und auch selbstgebautes Shadow DOM ist möglich! Im folgenden Codeschnipsel wird ein Shadow-DOM-Baum in ein ganz normales Element eingehängt. Dieses Shadow DOM erscheint dann wie ein fester Bestandteil des Elements, so wie die Steuerungselemente in einem HTML5-Videoplayer:

var widget = document.createElement('div');
var shadowRoot = widget.createShadowRoot();
var content = document.createElement('p');
content.innerHTML = 'Hallo Welt!';
shadowRoot.appendChild(content);
document.body.appendChild(widget);

Es ist klar, dass für Web Components, die ja ein komplexes Innenleben in selbstgebauten HTML-Elementen umsetzen wollen, Shadow DOM eine ganz zentrale Technologie darstellt.

HTML Imports

HTML Imports erlauben das Laden von HTML-Dokumenten in andere HTML-Dokumente:

<link rel="import" href="plugin.html">

Das ganze funktioniert nicht direkt wie z.B. ein include() in PHP o.Ä., sondern eher ein bisschen wie <template>-Elemente. Der Browser lädt das verlinkte Dokument herunter, liest es ein, lädt alle dort referenzierten Ressourcen (CSS, JS etc.) und verarbeitet das HTML. Das Resultat wird aber nicht einfach an Ort und Stelle in die Seite gekippt, sondern in der DOM-Eigenschaft import bereitgestellt. Hier findet man ein komplettes Document-Objekt für die geladene Ressource vor und kann sich nach Herzenslust daran bedienen: enthaltene Templates klonen, eingebundene Scripts ausführen  – was auch immer gerade anliegt.

Im Web-Component-Kontext kann ein solcher Import alle Daten für ein Element enthalten: die nötigen Templates, das Script zum Anmelden eines neuen Tags und was sonst noch so gebraucht wird. Für alle, die neue Elemente nur benutzen und nicht unbedingt selbst schreiben wollen, reduziert sich also der gesamte Arbeitsaufwand auf das Einbinden eines einzigen <link>-Elements (und natürlich das Einbinden des Importierten Custom Elements selbst).

Diverse weitere Technologien

Es gibt noch diverse weitere neue Browser-APIs, die oft mit Web Components assoziiert werden – Object.observe(), Scoped Style Sheets und viele weitere. Inwiefern man das alles braucht, kommt ein bisschen darauf an, wie genau man an Web Components herangeht, denn theoretisch führen viele Wege nach Rom – im Prinzip auch heute schon.

Was kann man heutzutage schon mit Web Components anfangen?

Den meisten Browsern fehlen noch die meisten Technologien für Web Components. In Chrome wird ab Version 36 so gut wie alles fast komplett implementiert sein, aber auch nur fast – und von den anderen Browsern wollen wir lieber gar nicht reden. Allerdings lässt sich mit einer Mixtur aus Polyfill und Abstraktion das Meiste durchaus schon hinkriegen. Dinge wie ein <google-map>-Element sind keine Papiertiger, sondern tatsächlich heute schon exakt so benutzbare Web Components. Und die Benutzung und Erstellung solcher Components passiert heute schon auf eine Weise, die recht nah an der zukünftigen Alltagspraxis sein dürfte. Natürlich braucht man heutzutage Polyfills und Libraries, doch die wird man auch in 5 Jahren noch brauchen – selbst wenn jeder Browser alle Web-Component-APIs vollständig unterstützt.

Nicht vergessen: „Web Components“ ist nur ein Überbegriff für viele verschiedene Technologien, die das Erstellen eigener Komponenten ermöglichen. Diese Technologien sind aber nicht allein auf den Komponenten-Bau ausgerichtet, sondern man kann sie für viele verschiedene Dinge benutzen. Es braucht immer noch einen JavaScript-Layer, der die Einzelteile zu einer komfortablen Component-API zusammenknotet. Entsprechend wird man auch in fernster Zukunft eine JavaScript-Library brauchen, die das Erstellen eigener Elemente mit Shadow DOM und Konsorten vereinfacht, eine Art jQuery für Eigenbau-Elemente. Heutzutage kommt auf diese Library einfach noch ein Haufen Polyfills obendrauf. Hat man aber einerseits die Polyfills, andererseits die Library und keine Angst vor experimenteller Software, so kann man heutztage durchaus schon seine eigenen Elemente bauen und in den meisten modernen Browsern herumspielen. Wie genau das konkret funktioniert, sehen wir dann in Teil 2 dieser kleinen Serie.

Erklärbär-Termine für Juni und Juli 2014

Talks, Kurzseminare und und Druckbetankungen zu den gewohnten Themen liefert der Webtechnologie-Erklärbär auch diesem Sommer reichlich:

  • 23. und 24. Juni in München: CSS3 bei der Open Source School. Mein bewährtes zweitä­gi­ges CSS3-Standardprogramm katapultiert die Teilnehmer in das CSS3-Zeitalter, in dem Webfonts und Farbverläufe fließen. Geboten wird ein großer Praxisanteil, kleine Arbeitsgruppen und mindestens ein CSS3-E-Book gibt es als Bonus.
  • 25. - 27. Juni 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. Auch hier steht einen großer Praxisanteil mit überschaubaren Arbeitsgruppen auf dem Plan und ein Exemplar des HTML5-Buchs gibt es obendrein.
  • 28. und 29. Juni in Grainau: HTML5/CSS3-Seminar auf dem Publishing BootCamp 2014. Ein anfängerfreundlicher Einstieg in die wunderbare Welt der modernen Webplattform.
  • 16. und 17. Juli in Nürnberg: Talk und Workshop zum Thema „Neue Techniken für Responsive Design“ auf der Developer Week 2014. Geplant ist eine Tour in die äußeren Randgebiete von Responsive Web Design. Techniken von Morgen, APIs der Zukunft und die Herangehensweise für eine Post-PC-Welt stehen auf der Agenda.

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

HTML5 Drag & Drop, Teil III: der indirekte Drop-Nachweis

Dieser Artikel ist Teil einer Serie:

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

Der Begriff „Planet“ geht auf das griechische Wort πλανήτης zurück, das ganz grob übersetzt „Umherschweifender“ bedeutet. Das Wort hat sich durchgesetzt, weil schon die alten Griechen die Planeten beobachten konnten und erkannten, dass diese sich am Himmel bewegen, im Gegensatz zu ganz normalen Sternen. Nur der Planet Neptun ist in der Antike nie entdeckt worden, weil er einfach viel zu weit entfernt ist, um ohne weiteres als bewegtes Objekt erkannt zu werden. Einem findigen Mathematiker namen Urbain Le Verrier fiel allerdings 1846 auf, dass die Umlaufbahn des bis dahin als äußerstem Planeten bekannten Uranus ein wenig seltsam war. Uranus verhielt sich so, als gäbe es jenseits seiner Bahn noch eine weitere beträchtliche Gravitationsquelle – sowas wie einen großen Planeten. Le Verrier rechnete aus, wo dieser mysteriöse Extra-Planet zu einer bestimmten Uhrzeit zu sehen sein müsste, setzte jemanden vor ein auf die fragliche Stelle gerichtetes Teleskop und siehe da– der Planet Neptun ward entdeckt. Und wenn wir die Drag & Drop-API von HTML5 komplett bändigen wollen, müssen wir genau wie Monsieur Le Verrier einen indirekten Nachweis führen, nur eben über Geschehnisse im Browser und nicht über astronomische Objekte.

Sinn und Zweck des Drop Effect

Eine Drag & Drop-Operation kann vieles bedeuten. Wenn Element A auf Element B gezogen wird, könnte das eine Kopier- oder auch eine Verschiebe-Operation sein. Man kennt das vom Datei-Manager seines Betriebssystems – je nachdem welche Taste man gedrückt hält, wird eine Datei verschoben oder kopiert oder macht ganz etwas anderes. Die HTML5-API erlaubt es, die mit einer Operation verbundenen Intention (kopieren, verschieben etc.) durch verschiedene Event-Stufen zu transportieren, ähnlich wie Daten transportiert werden. Und ähnlich wie die Daten lässt sich diese Intention bei einigen der Events im Rahmen einer Drag-Operation auslesen – natürlich nicht bei allen Events und auch nicht bei den gleichen Events wie die Daten, aber es geht. Gemäß der Spezifikationen lässt sich die Intention bei den Events dragenter und dragover sowie ggf. bei drop und dragend auslesen, wobei sie jeweils im Event-Objekt in der Eigenschaft dropEffect zu finden ist.

Der dropEffect kann die Werte none, copy, move und link haben, die für jeweils eine Intention stehen. Wie das funktioniert kann man am besten einer einer simplen Demo mit gezogenem Text sehen. Nicht vergessen: die Drag & Drop-API von HTML5 ist die gleiche Funktionalität, die im Browser auch die ganz normalen Drag-Operationen abwickelt. Demnach müsste man in dieser Fiddle unterschiedliche Werte gemeldet bekommen, je nachdem wie man Text auf das Ziel-Div zieht. Drückt man während der Operation die jeweilige Modifikator-Taste (variiert je nach OS; meist ist es ctrl oder alt), so erhält man copy, andernfalls move – immer vorausgesetzt, der Browser verhält sich diebezüglich standardkonform, was Mitte 2014 einzig beim Firefox der Fall ist.

Diese kleine Demo ist kein sinnvolles Beispiel für die Nutzung des Drop Effect, zeigt aber den Gedanken hinter dieser Eigenschaft. Wenn wir den erst mal verstanden haben, können wir den Drop Effect für unsere eigenen Zwecke in unseren eigenen Drag-Operationen nutzen, denn wir können ihn auch selbst setzen … oder besser gesagt beeinflussen.

Den Drop Effect beeinflussen

Bei einer selbstgebauten Drag & Drop-Operation wie unserem Endprodukt aus Teil 2 der Artikelserie können wir im dragstart-Event uns selbst den Drop Effect für unsere Operation aussuchen. Das Problem ist, dass das nicht ganz so einfach ist, wie man vielleicht spontan hoffen mag. Das, was später der Drop Effect wird, wird über eine Eigenschaft namens effectAllowed festgelegt, die nicht die gleichen Werte wie der spätere dropEffect hat. Stattdessen akzeptiert er die Werte none, copy, copyLink, copyMove, link, linkMove, move, all und uninitialized. Jeder dieser Werte resultiert in einem oder mehreren bestimmten Drop Effects, die dieser Tabelle zu entnehmen sind:

effectAllowed mögliche dropEffects
copy copy
move move
link link
copyLink copy, link
copyMove copy, move
linkMove link, move
all copy, link, move

Es gibt noch ein paar weitere effectAllowed-nach-dropEffect-Mutier-Pfade, die jedoch für unseren Use Case von gezogenen Elementen erst mal nicht relevant sind. Man erkennt aber an der Tabelle ein System. Jene effectAllowed-Werte, die auch ein gültiger dropEffect-wären, resultieren in eben genau diesem Wert als dropEffect. Die Kombinationswerte wie copyMove lassen als dropEffect sowohl copy als auch move zu – es kommt dann darauf an, wie genau der Nutzer das Element gezogen hat (d.h. welche Tasten während der Operation gedrückt wurden).

An dieser Stelle können wir an die (z.Z. nur im Firefox funktionierende) Text-Drag-Demo von vorhin andocken, dann das Ganze funktioniert genau so mit Elementen! Man setze beim Start der Drag-Operation einen effectAllowed, der zwei verschiedene dropEffect-Werte zulässt …

$('li').on('dragstart', function(evt){
  evt.originalEvent.dataTransfer.setData('text', '');
  evt.originalEvent.dataTransfer.effectAllowed = 'copyMove';
});

… und schon wird uns je nachdem wie wir das Element ziehen (normal oder mit gedrückter ctrl/alt-Taste) im drop-Event mal der eine, mal der andere dropEffect gemeldet:

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  $(this).removeClass('valid');
  window.alert(evt.originalEvent.dataTransfer.dropEffect);
});

Im Firefox ausprobieren! Blöderweise machen es die anderen Browser weniger gut; Chrome und Safari liefern beide beim drop-Event immer none, ganz egal wie der Nutzer das Element durch die Gegend zieht. IE macht es zumindest mit gezogenen Textauswahlen richtig. Je nachdem was wir nun mit dem dropEffect anzustellen gedenken, kann uns das stören oder auch nicht.

Den Drop Effect verwenden

Der dropEffect ist selbst in den Browsern, die ihn nur mittelprächtig implementieren nützlich, doch fokussieren wir uns zunächst auf einen dem Standard entsprechenden Use Case. Dieser wird dann in Firefox, Chrome und Safari funktionieren. Man kann mit kleinen Einschränkungen auch andere Browser unterstützen, aber wir fangen erst mal mit dem vollen Programm an.

Wir können in das Endprodukt aus Teil 2 der Artikelserie recht einfach einen dropEffect einbauen, indem wir bei dragstart den effectAllowed festlegen und und beim drop den dropEffect auslesen. Das wäre für sich genommen erst mal ziemlich nutzlos. Allerdings könnten wir ja tatsächlich die beiden denkbaren effectAllowed-Werte das machen lassen, was sie versprechen – nämlich die gezogenen Elemente kopieren oder verschieben! Der erste Schritt ist das schon erwähnte Einbauen des effectAllowed in dragstart:

$('li').on('dragstart', function(evt){
  var type = $(this).attr('data-type');
  var data = $(this).text();
  evt.originalEvent.dataTransfer.setData(type, data);
  // Kopieren und verschieben zulassen
  evt.originalEvent.dataTransfer.effectAllowed = 'copyMove';
});

Beim drop-Event könnte man nun den dropEffect auslesen und das gezogene Element entweder klonen und in das Ziel-Element einhängen (dropEffect ist copy) oder einfach das gezogene Element direkt adoptieren (dropEffect ist copy). Das ist allerdings aus einer Reihe von Gründen nicht empfehlenswert – nicht zuletzt, weil wir in der Demo <li>-Elemente ziehen und diese keine direkten Kinder von <div>-Elementen sein dürfen. Besser ist es, das gezogene Element anhand der im dataTransfer-Objekt übertragenen Daten nachzubauen:

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  $(this).removeClass('valid');
  var key = $(evt.target).attr('data-accept');
  var val = evt.originalEvent.dataTransfer.getData(key);
  // Das gezogene Element als Div "nachbauen"
  $('<div>').text(val).appendTo(evt.target);
});

Der Code deckt nun den copy-Fall ab, jedes gezogene Element wird geklont. Für den move-Fall können wir das dragend-Event nutzen, das auf dem gezogenen Element feuert. Hier fragen wir ab, ob der dropEffect den Wert move hat und entfernen in diesem Fall einfach das gezogene Element:

$('li').on('dragend', function(evt){
  if(evt.originalEvent.dataTransfer.dropEffect === 'move'){
    // Im Fall von Verschiebe-Operation das gezogene Element löschen
    $(evt.target).remove();
  }
});

Fertig! Je nach gedrückter Modifikator-Taste werden die ziehbaren Elemente nun in ihr Ziel-Element kopiert oder verschoben – und das immerhin in Chrome, Firefox und Safari. Und es kommt noch besser: das Script funktioniert jetzt über Browserfenster-Grenzen hinweg! Man kann zwei Firefox-Fenster mit der Fiddle geöffnet haben und von einem Fenster ins andere Elemente ziehen um sie zu kopieren und zu verschieben.

Das Ganze funktioniert aus drei Gründen: zum einen ist die API einfach so gebaut, dass sie ganz allgemei Drag & Drop-Operationen verarbeitet. Wo diese Operationen starten, ob im gleichen Browserfenster, in einem anderen Fenster oder ob es sich um eine vom Desktop gezogene Datei handelt, ist der API egal. Zweitens stören uns die Bugs in Chrome und Safari, die im drop-Event den falschen dropEffect melden nicht, da wir ihn nur im dragend-Event abfragen, wo er korrekt ausgegeben wird. Und drittens anderen ist der Code so geschrieben, dass jedes Event nur mit den Daten arbeitet, die die API ihm bereitstellt. Würden wir z.B. das gezogene Element im drop-Event nicht nachbauen, sondern das wirklich gezogene Element adopieren, so könnte das nicht funktionieren – wir bekommen schließlich in Fenster A keinen Zugriff auf Elemente in Fenster B. Nur die Daten der Drag-Operation werden übermittelt, wobei diese dann auch reichen, um einigermaßen überzeugend ein Verschieben über Fenstergrenzen hinweg zu simulieren.

Der große Haken an unserem bisherigen Werk ist, dass es aktuell nur in den modernsten Varianten von Firefox, Chrome und Safari funktioniert. Das lässt sich auch nicht ohne weiteres ändern, allerdings können wir mit einer kleinen Einschränkung des Funktionsumfangs weitere Browser bedienen.

Der eingeschränkte Drop Effect als indirekter Drop-Nachweis

Browser wie der Internet Explorer liefern, egal welche Tasten man drückt, den falschen dropEffect. Aber immerhin: sie liefern einen, vorausgesetzt es hat ein drop-Event stattgefunden. Damit kann man zwar nicht mehr zwischen Kopier- und Verschiebe-Operationen unterscheiden, aber immer noch Elemente von Browserfenster A nach Fenster B ziehen. So wird der dropEffect zu einem einfachen Signal degradiert, mit dem wir in einem Fenster festellen können, was im anderen passiert ist, so wie Urbain Le Verrier die abweichende Bahn von Uranus das Signal für die Existenz von Neptun war.

Der dropEffect ist laut Standard none wenn das drop-Event nicht stattgefunden hat (weil z.B. die dragover-Handbremse nicht gelöst wurde). Wenn das drop-Event stattgefunden hat, ist der dropEffect einer der Werte aus dem effectAllowed-Setting – im Ideallfall der Wert, der der durch Tastendruck untermauerten Intention entspricht. Der IE macht nur den letzten Teil falsch, d.h. browserfensterübergreifende Operationen klappen noch immer! Es gibt nur keine Unterscheidung zwischen verschiedenen Daten (da ja der IE als Daten-Key ausschließlich text zulässt) und Intentionen. Ein entsprechend vereinfachter Fall funktioniert also auch im Internet Explorer!

Fazit

Drag & Drop ist sowohl in nativen Programmen als auch in Webapps ein alter Hut. Die Möglichkeiten im Browser waren aber stets beschränkt. Datenaustausch zwischen Browserfenstern oder einfach nur mehreren Teilen einer App (z.B. iFrames) war kaum möglich. Auch das Ausdrücken von Intention per Tastendruck war bestenfalls knifflig, da ja jedes Betriebssystem-Interface hier seine eigenen Regeln aufstellt. Um hier auf das native Level zu kommen brauchte es zwingend eine Browser-API.

Mit HTML5 wird das Web in Sachen Drag & Drop-Fähigkeiten zumindest in die richtige Richtung geschoben. Theoretisch, d.h. auf dem Spezifikations-Papier, ist alles da, was man braucht und die mißratene API ließe sich mit einer kleinen Abstraktionsschicht in den Griff kriegen. Mir ist noch keine entsprechende Library bekannt, allerdings würde der flächendeckende Einsatz auch mit Astraktionsschicht am Intenet Explorer scheitern, der selbst in der neuesten Version gleich mehrere Features nicht unterstützt. Sollte sich das demnächst ändern, sind auch im IE mit Webapps komplizierte Dateisortier-Interfaces oder Multi-Fenster-Apps möglich … vorausgesetzt ein findiger JavaScript-Nerd bringt die API mal in benuztzbare Form.

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

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.

Folgt mir!

Kauft mein Zeug!

Cover der HTML5-DVD
Infos auf html5-dvd.de, kaufen bei Amazon

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover des CSS3-Buchs
Infos auf css3-buch.de, kaufen bei Amazon