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.