Natives Drag & Drop in HTML5

HTML5 Rocks

Einführung

Jahrelang haben wir Bibliotheken wie JQuery und Dojo zur Vereinfachung von komplexen Elementen der Benutzeroberfläche wie Animationen, abgerundeten Ecken und Drag & Drop eingesetzt. Ein Blickfang ist für überzeugende, immersive Erfahrungen im Web zweifellos wichtig. Doch warum sollte für gängige Aufgaben, die von allen Entwicklern durchgeführt werden, eine Bibliothek erforderlich sein?

Drag & Drop (DnD) ist ein HTML5-Spitzenmerkmal! In der Spezifikation sind ein Mechanismus auf Basis von Ereignissen, das JavaScript-API und eine zusätzliche Auszeichnung definiert, durch die deklariert wird, dass buchstäblich jede Art von Element auf einer Seite ziehbar (draggable) sein kann. Gegen nativen Browsersupport für eine bestimmte Funktion kann wohl niemand Einwände haben. Natives DnD bei Browsern führt zu schnelleren, reaktionsfähigeren Web-Apps.

Funktionserkennung

Viele Apps, in denen DnD eingesetzt wird, wären ohne diese Funktion nicht gerade benutzerfreundlich. Stellen Sie sich beispielsweise Schachfiguren vor, die sich nicht bewegen lassen. Hoppla! Auch bei einem schon recht umfassenden Browsersupport ist es wichtig zu bestimmen, ob ein Browser DnD – oder eine beliebige HTML5-Funktion zu diesem Zweck – implementiert, um eine Lösung mit ansprechendem Ausblendeffekt bereitstellen zu können. Wenn DnD nicht verfügbar ist, aktivieren Sie den Fallback der Bibliothek, damit die betreffende App weiter funktioniert.

Falls Sie ein API verwenden müssen, nutzen Sie grundsätzlich die Funktionserkennung, statt den Header "User-Agent" des Browsers zu durchsuchen. Eine der besseren Bibliotheken für die Funktionserkennung ist Modernizr. Modernizr legt für jede getestete Funktion eine boolesche Eigenschaft fest. Daher bedarf es zur Prüfung auf DnD nur einer Zeile:

if (Modernizr.draganddrop) {
  // Browser supports HTML5 DnD.
} else {
  // Fallback to a library solution.
}

Ziehbare Inhalte erstellen

Es ist ganz einfach, ein Objekt als ziehbar zu definieren. Stellen Sie für das Element, das verschiebbar sein soll, das draggable=true-Attribut ein. Die Drag-Funktion lässt sich für nahezu alles aktivieren, unter anderem für Bilder, Links, Dateien oder andere DOM-Knoten.

Als Beispiel soll zunächst die Erstellung von Spalten dienen, die neu angeordnet werden können. Die grundlegende Auszeichnung könnte zum Beispiel so aussehen:

<div id="columns">
  <div class="column" draggable="true"><header>A</header></div>
  <div class="column" draggable="true"><header>B</header></div>
  <div class="column" draggable="true"><header>C</header></div>
</div>

In den meisten Browsern sind ausgewählter Text sowie "img"- und "anchor"-Elemente mit einem href-Attribut standardmäßig ziehbar. Durch Ziehen des Logos auf google.com wird beispielsweise ein Doppelbild erzeugt:

Bild im Browser ziehen
Die meisten Browser unterstützen standardmäßig das Ziehen von Bildern.

das in der Adressleiste, einem <input type="file" />-Element oder sogar auf dem Desktop abgelegt werden kann. Zur Aktivierung der Ziehbarkeit für andere Arten von Inhalten benötigen Sie die HTML5-DnD-APIs.

Mit ein wenig CSS3-Magie können wir unsere Auszeichnung mit dem Aussehen von Spalten aufpolieren. Durch Hinzufügen von cursor: move wird Nutzern bei verschiebbaren Elementen ein entsprechender visueller Indikator angezeigt:

<style>
/* Prevent the text contents of draggable elements from being selectable. */
[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Required to make elements draggable in old WebKit */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}
.column {
  height: 150px;
  width: 150px;
  float: left;
  border: 2px solid #666666;
  background-color: #ccc;
  margin-right: 5px;
  -webkit-border-radius: 10px;
  -ms-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 3px #000;
  -ms-box-shadow: inset 0 0 3px #000;
  box-shadow: inset 0 0 3px #000;
  text-align: center;
  cursor: move;
}
.column header {
  color: #fff;
  text-shadow: #000 0 1px;
  box-shadow: 5px;
  padding: 5px;
  background: -moz-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -webkit-gradient(linear, left top, right top,
                               color-stop(0, rgb(0,0,0)),
                               color-stop(0.50, rgb(79,79,79)),
                               color-stop(1, rgb(21,21,21)));
  background: -webkit-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  background: -ms-linear-gradient(left center, rgb(0,0,0), rgb(79,79,79), rgb(21,21,21));
  border-bottom: 1px solid #ddd;
  -webkit-border-top-left-radius: 10px;
  -moz-border-radius-topleft: 10px;
  -ms-border-radius-topleft: 10px;
  border-top-left-radius: 10px;
  -webkit-border-top-right-radius: 10px;
  -ms-border-top-right-radius: 10px;
  -moz-border-radius-topright: 10px;
  border-top-right-radius: 10px;
}
</style>

Ergebnis – ziehbar, ohne das etwas geschieht:

A
B
C

Im Beispiel oben wird von den meisten Browsern ein Doppelbild des gezogenen Inhalts erzeugt. Bei anderen Browsern, insbesondere FF, müssen während des Drag-Vorgangs Daten gesendet werden. Im nächsten Abschnitt beginnen wir damit, unser Spaltenbeispiel etwas interessanter zu gestalten. Dazu fügen wir Listener für die Verarbeitung des Drag/Drop-Ereignismodells hinzu.

Horchen auf Ziehereignisse

Es gibt eine Reihe von verschiedenen Ereignissen zur Überwachung des gesamten Drag & Drop-Vorgangs:

  • dragstart
  • drag
  • dragenter
  • dragleave
  • dragover
  • drop
  • dragend

Zur Handhabung des DnD-Ablaufs müssen wir eine Vorstellung vom Quellelement haben, das heißt vom Ursprung des gezogenen Elements, von der Datennutzlast, also vom zu ziehenden Element, und vom Ziel, das heißt vom Bereich, der das abgelegte Element aufnimmt. Das Quellelement kann eine Liste, ein Bild, Link, Dateiobjekt, HTML-Block oder beliebiges anderes Element sein. Das Ziel ist die Drop-Zone oder eine Gruppe von Drop-Zonen, die die Daten aufnimmt, die der Nutzer abzulegen versucht. Vergessen Sie nicht, dass nicht alle Elemente – beispielsweise Bilder – Ziele sein können.

1. Drag-Vorgang starten

Nachdem Sie draggable="true"-Attribute für die Inhalte definiert haben, fügen Sie dragstart-Ereignis-Handler hinzu, um die DnD-Sequenz für jede Spalte zu beginnen.

Durch diesen Code wird die Deckkraft der Spalte auf 40 % festgelegt, wenn der Nutzer beginnt, sie zu ziehen:

function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
});

Ergebnis:

A
B
C

Ziel des dragstart-Ereignisses ist unser Quellelement. Daher erhält der Nutzer durch die Einstellung von this.style.opacity auf 40 % ein visuelles Feedback dafür, dass die aktuelle Auswahl verschoben wird. Nach Abschluss des Drag-Vorgangs müssen wir die Deckkraft der Spalten wieder auf 100 % einstellen. Hierzu bietet sich das dragend-Ereignis an. Weitere Infos zu diesem Thema folgen später.

2. "dragenter", "dragover" und "dragleave"

Mit dragenter-, dragover- und dragleave-Ereignis-Handlern können Sie während des Drag-Vorgangs weitere visuelle Hinweise bereitstellen. Wenn beispielsweise während eines Drag-Vorgangs die Maus über einer Spalte bewegt wird, könnten ihre Ränder gestrichelt angezeigt werden. Hierdurch erfahren die Nutzer, dass die Spalten auch Drop-Ziele sind.

<style>
.column.over {
  border: 2px dashed #000;
}
</style>
function handleDragStart(e) {
  this.style.opacity = '0.4';  // this / e.target is the source node.
}

function handleDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }

  e.dataTransfer.dropEffect = 'move';  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnter(e) {
  // this / e.target is the current hover target.
  this.classList.add('over');
}

function handleDragLeave(e) {
  this.classList.remove('over');  // this / e.target is previous target element.
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false);
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
});

In diesem Code gibt es eine Reihe von Punkten, deren genauere Betrachtung sich lohnt:

  • this/e.target ändert sich für jede Art von Ereignis, je nachdem, an welcher Stelle im DnD-Ereignismodell wir uns befinden.
  • Beim Ziehen eines Elements, etwa eines Links, muss das Standardverhalten des Browsers unterbunden werden, nämlich dass zu diesem Link navigiert wird. Rufen Sie dazu e.preventDefault() im dragover-Ereignis auf. Eine weitere bewährte Praxis ist die Verwendung des return false-Befehls im selben Handler. Diese Maßnahmen sind zwar nicht bei allen Browsern erforderlich, doch es schadet nicht, sie durchgängig hinzuzufügen.
  • Mit dragenter wird die 'over'-Klasse anstelle von dragover umgeschaltet. Wenn wir dragover verwenden müssten, würde unsere CSS-Klasse häufig umgeschaltet, weil das dragover-Ereignis beim Bewegen der Maus über der Spalte weiterhin ausgelöst würde. Dies würde letztlich dazu führen, dass der Renderer des Browsers einer hohen unnötigen Arbeitsbelastung ausgesetzt wäre. Es empfiehlt sich stets, die Anzahl der Aktualisierungen auf ein Minimum zu beschränken.

3. Drag-Vorgang abschließen

Zur Verarbeitung des eigentlichen Drop-Vorgangs fügen Sie einen Ereignis-Listener für die drop- und dragend-Ereignisse hinzu. In diesem Handler muss das Standardverhalten für Drop-Vorgänge unterbunden werden, in der Regel eine lästige Weiterleitung. Sie können das Eintreten des Ereignisses in DOM durch einen Aufruf von e.stopPropagation() verhindern.

Bei unserem Spaltenbeispiel würde ohne das drop-Ereignis nicht viel geschehen. Zuvor entfernen wir aber mithilfe von dragend die 'over'-Klasse aus jeder Spalte und sorgen so für eine sofortige Verbesserung:

...

function handleDrop(e) {
  // this / e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // stops the browser from redirecting.
  }

  // See the section on the DataTransfer object.

  return false;
}

function handleDragEnd(e) {
  // this/e.target is the source node.

  [].forEach.call(cols, function (col) {
    col.classList.remove('over');
  });
}

var cols = document.querySelectorAll('#columns .column');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', handleDragStart, false);
  col.addEventListener('dragenter', handleDragEnter, false)
  col.addEventListener('dragover', handleDragOver, false);
  col.addEventListener('dragleave', handleDragLeave, false);
  col.addEventListener('drop', handleDrop, false);
  col.addEventListener('dragend', handleDragEnd, false);
});

Ergebnis:

A
B
C

Wenn Sie bis hierhin aufmerksam gefolgt sind, haben Sie möglicherweise bemerkt, dass die Spalte in unserem Beispiel immer noch nicht erwartungsgemäß abgelegt wird. Hier kommt das DataTransfer-Objekt ins Spiel.

Das DataTransfer-Objekt

Bei DnD spielt die dataTransfer-Eigenschaft eine zentrale Rolle. Sie enthält die bei einer Drag-Aktion gesendeten Daten. dataTransfer wird im dragstart-Ereignis festgelegt und im drop-Ereignis gelesen bzw. verarbeitet. Durch einen Aufruf von e.dataTransfer.setData(format, data) wird der Inhalt des Objekts auf den MIME-Typ und die Datennutzlast festgelegt, die als Argumente weitergegeben werden.

In unserem Beispiel wird die Datennutzlast auf die HTML-Codierung der Quellspalte festgelegt:

var dragSrcEl = null;

function handleDragStart(e) {
  // Target (this) element is the source node.
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

Praktischerweise verfügt dataTransfer auch über eine getData(format)-Methode zum Abruf der Drag-Daten anhand des MIME-Typs. Hier sehen Sie die Änderung zur Verarbeitung des Drop-Vorgangs für die Spalte:

function handleDrop(e) {
  // this/e.target is current target element.

  if (e.stopPropagation) {
    e.stopPropagation(); // Stops some browsers from redirecting.
  }

  // Don't do anything if dropping the same column we're dragging.
  if (dragSrcEl != this) {
    // Set the source column's HTML to the HTML of the columnwe dropped on.
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

Zur Vereinfachung des Spaltenaustauschs habe ich eine globale Variable dragSrcEl hinzugefügt. In handleDragStart() wird das innerHTML-Element der Quellspalte in dieser Variablen gespeichert und später in handleDrop() gelesen, um den HTML-Code der Quell- und Zielspalte auszutauschen.

Ergebnis:

A
B
C

Dragging-Eigenschaften

dataTransfer stellt Eigenschaften für ein visuelles Feedback an den Nutzer während des Drag-Vorgangs bereit. Mit diesen Eigenschaften lässt sich außerdem steuern, wie jedes Drop-Ziel auf einen bestimmten Datentyp reagiert.

dataTransfer.effectAllowed
Beschränkt, welche Art von Drag-Vorgang der Nutzer für das Element ausführen kann. Diese Eigenschaft wird im Drag & Drop-Verarbeitungsmodell verwendet, um dropEffect während der dragenter- und dragover-Ereignisse zu initialisieren. Sie kann auf folgende Werte eingestellt werden: none, copy, copyLink, copyMove, link, linkMove, move, all und uninitialized.
dataTransfer.dropEffect
Steuert das Feedback, das der Nutzer während der dragenter- und dragover-Ereignisse erhält. Wenn der Nutzer die Maus über ein Zielelement bewegt, zeigt der Cursor des Browsers an, welche Art von Vorgang stattfinden wird, beispielsweise ein Kopiervorgang, eine Verschiebung und so weiter. Der Effekt kann einen der folgenden Werte annehmen: none, copy, link und move.
e.dataTransfer.setDragImage(img element, x, y)
Statt des standardmäßigen Doppelbilds des Browsers können Sie optional ein Ziehsymbol als Feedback festlegen:
var dragIcon = document.createElement('img');
dragIcon.src = 'logo.png';
dragIcon.width = 100;
e.dataTransfer.setDragImage(dragIcon, -10, -10);

Ergebnis – Sie sollten beim Ziehen dieser Spalten das Google-Logo sehen:

A
B
C

Dateien ziehen

Bei den DnD-APIs ist es möglich, Dateien im Browserfenster vom Desktop in Ihre Web-App zu ziehen. Google Chrome greift diese Idee auf und unterstützt die Möglichkeit, Dateiobjekte aus dem Browser auf den Desktop zu ziehen.

Hineinziehen: Vom Desktop in den Browser ziehen

Mithilfe von DnD-Ereignissen als weiteren Inhaltsarten lässt sich eine Datei vom Desktop ziehen. Der Hauptunterschied liegt im drop-Handler. Es wird nicht mit dataTransfer.getData() auf die Dateien zugegriffen, sondern die Daten sind in der dataTransfer.files-Eigenschaft enthalten:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; f = files[i]; i++) {
    // Read the File objects in this FileList.
  }
}

Eine vollständige Anleitung dazu, wie Sie Dateien vom Desktop in den Browser ziehen, finden Sie unter Verwenden von Drag & Drop für die Auswahl in "Lesen lokaler Dateien in JavaScript".

Herausziehen: Ziehen vom Browser auf den Desktop

Eine vollständige Anleitung dazu, wie Sie Dateien vom Browser auf den Desktop ziehen, finden Sie unter Drag out files like Gmail" (Dateien wie Google Mail herausziehen) im CSS-Ninja.

Beispiele

Hier sehen Sie das fertige Produkt, das nun ein wenig aufpoliert und mit einem Zähler für jede Verschiebung versehen ist:

A
B
C
D

Beim Spaltenbeispiel besteht der interessante Aspekt darin, dass die Spalten sowohl Drag-Quelle als auch Drop-Ziel sind. In einem gängigeren Szenario sind die Quell- und Zielelemente nicht identisch. Eine Demo finden Sie unter html5demos.com/drag.

Fazit

Niemand wird bestreiten, dass das DnD-Modell von HTML5 im Vergleich zu anderen Lösungen wie der JQuery-Benutzeroberfläche ziemlich komplex ist. Wenn Sie jedoch die nativen APIs des Browsers nutzen können, sollten Sie dies grundsätzlich tun! Dies ist letzten Endes der Aspekt, der HTML5 ausmacht: Es geht um das Standardisieren und Verfügbarmachen einer Gruppe von leistungsfähigen APIs, die nativ in den Browser integriert sind. Irgendwann einmal werden gängige Bibliotheken mit implementierter DnD-Funktionalität hoffentlich standardmäßig nativen Support für HTML5 und Fallback-Merkmale zur bedarfsgerechten Anpassung einer JavaScript-Lösung umfassen.

Referenzen

Comments

0