JavaScript/Drag & Drop

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Wussten Sie eigentlich schon, dass sich Bilder, Anker und markierter Text auch ohne spezielle Festlegung mit der Maus ziehen lassen? Probieren Sie es mit dem Logo links oben und den Elementen dieser Webseite einmal aus! Es ist allerdings nicht möglich, diese Elemente irgendwo abzulegen (mit Ausnahme eines input-Elements).

Seit die Computer-Maus das Ziehen und Ablegen von Bildern und Icons ermöglichte, erfreut sich diese intuitive und benutzerfreundliche Methode großer Beliebtheit. Dieses Drag & Drop wurde von Microsoft schon 1999 im IE5 eingeführt. In HTML5 sorgt die Drag & Drop-API nun für nativen Browser-Support, ohne dass externe Frameworks notwendig sind.[1]

Trotz kritischer Stimmen, die die Menge der möglichen Events und die umständliche Handhabung beim Ablegen bemängeln, ist eine einheitliche Spezifikation und nativer Browser-Support doch zu begrüßen.[2] Grobe Ungereimtheiten wie eine CSS-Deklaration -khtml-user-drag: element; als Ersatz für das damals in Safari fehlende draggable-Attribut sind mittlerweile nicht zuletzt dank der einheitlichen Spezifikation beseitigt.

Beachten Sie: Auch wenn die Browser-Unterstützung gut aussieht, ist HTML5-Drag & Drop auf mobilen Geräten mit iOS und Android (noch) nicht möglich.[3]


HTML

Eigentlich ist Drag und Drop eine HTML5-API, die bereits im HTML ansetzt. Dafür gibt es ein neues Universalattribut. Ein zweites wurde vorgeschlagen, aber nicht implementiert und wieder aus dem Standard entfernt.

draggable

Mit dem neuen Universalattribut draggable="true" können Sie jedes Element ziehbar machen, bzw. das Ziehen von Bildern und Links durch Setzen von draggable="false" unterbinden.

Folgende Werte sind erlaubt:

  • true: das Element kann gezogen und verschoben werden
  • false: das Element kann nicht gezogen und verschoben werden
Elemente ziehbar machen ansehen …
<img id="drag1" src="Selfhtml-logo.gif" alt="Spinnen-Logo" draggable="false" ondragstart="drag(event)">
<img id="drag2" src="2006_patrick_rudolph.gif" draggable="true" ondragstart="drag(event)">
<img id="drag3" src="2006-ani_kirsten-evers.gif" draggable="true" ondragstart="drag(event)">

Im Beispiel sind die beiden Weihnachts-Logos mit draggable=true ziehbar gemacht worden.

Um zu verdeutlichen, was gezogen werden darf und was nicht, können Sie mit CSS die Darstellung ändern:

Mauszeiger ansehen …
[draggable=true] {
  cursor: move;
}
[draggable=false] {
  cursor: not-allowed;
}

Mit der CSS-Eigenschaft cursor:move wird der Maus-Zeiger während des Ziehens angepasst. Die Einstellung cursor: not-allowed; signalisiert bei draggable="false" das Ziehverbot.

Erzeugen einer Ablagezone (das ehemalige dropzone Attribut)

Ohne besondere Maßnahmen zu ergreifen, können Sie gezogene Objekte in input- und textarea- sowie mit contenteditable ausgezeichnete Elementen ablegen.

Das dropzone-Attribut war dafür vorgesehen, weitere Ablageorte für gezogene Objekte deklarativ im Markup festzulegen. Die Versionen 5 und 5.1 des HTML Standards haben es aufgeführt, es wurde aber in keinem Browser implementiert und 2017 mit HTML 5.2 entfernt.

Die Begründung für die Entfernung war: Man muss dafür ohnehin JavaScript schreiben. Und dann kann man auch Script verwenden, um den Drop zu ermöglichen. Das geschieht so, dass man einen Handler für das dragover-Event registriert und für einen erwünschten Drop das Standardverhalten des Browsers (das im Verhindern des Drop besteht) unterbindet:

Ablageort festlegen ansehen …
<div id="ziel" ondrop="drop(event)" ondragover="allowDrop(event)"></div>

<script>
function allowDrop(ev) {
  ev.preventDefault();
}

function drag(ev) {
  ev.dataTransfer.setData('text', ev.target.id);
}

function drop(ev) {
  ev.preventDefault();
  let data = ev.dataTransfer.getData('text');
  ev.target.appendChild(document.getElementById(data));
}
</script>

JavaScript

Bereits hier ist klar, dass Drag und Drop nur mit dem Einsatz von JavaScript funktioniert.

drag-Events

Die API bietet eine Vielzahl von Events, um das Ziehen und Ablegen möglichst genau steuern zu können.

  • dragstart: Sobald Sie ein Element mit gedrückter Maus ziehen, wird der dragstart-Event gefeuert. Er übergibt dem dataTransfer-Objekt die nötigen Informationen.
  • drag: feuert, solange Sie ein Element oder eine Textauswahl ziehen. Das mit event.Target ermittelte Target ist das bewegte Element.
  • dragover: löst aus, wenn Sie ein Element oder eine Textauswahl auf/über ein gewünschtes Ziel gezogen haben. Das mit event.Target ermittelte Target ist das Element, über dem man gerade mit dem bewegten Element schwebt.
    Beachten Sie: Das ist ein entscheidender Unterschied, denn in dragover kann man prüfen, ob der Cursor gerade über einem drop-fähigen Element schwebt (mangels droppable-Attribut) und entsprechend den dropEffect setzen.
  • dragenter: löst aus, wenn Sie ein Element oder eine Textauswahl zum gewünschten Ziel gezogen haben.
  • dragleave: dragleave feuert, wenn Sie ein Element oder eine Textauswahl wieder vom gewünschten Ziel wegziehen.
  • drop: löst aus, wenn Sie ein Element oder eine Textauswahl abgelegt haben.
  • dragend: wird ausgelöst, wenn der Ziehvorgang vorzeitig abgebrochen wird.
Beispiel ansehen …
const anzeige1 = document.getElementById('anzeige1'),
      anzeige2 = document.getElementById('anzeige2'),
      anzeige3 = document.getElementById('anzeige3'),
      anzeige4 = document.getElementById('anzeige4');

document.addEventListener('dragstart', function(event) {
    event.dataTransfer.setData('Text', event.target.id);
    anzeige1.innerHTML = 'dragstart: ausgelöst';
    anzeige1.className = 'start';
});

document.addEventListener('drop', function (event) {
    event.preventDefault();
    let dropziel = event.target.closest('.dropziel');
    if (dropziel) {
        let draggedId = event.dataTransfer.getData('Text');
        dropziel.appendChild(document.getElementById(draggedId));
        // ...
    }
});

document.addEventListener('drag', function(event) {
    anzeige2.innerHTML = 'drag: wird gezogen';
    anzeige2.className = 'aktion';
});

document.addEventListener('dragenter', function(event) {
    let dropziel = event.target.closest('.dropziel');
    if (dropziel) {
        anzeige4.innerHTML = 'dragenter:<br> "Sie haben ihr Ziel erreicht"';
        anzeige4.className = 'ziel';
        dropziel.classList.add('erreicht');
    }
});
document.addEventListener('dragover', function (event) {
    event.preventDefault();
    anzeige3.innerHTML = 'dragover: über einem HTML-Element!<br> event.Target:' + event.target.nodeName;
});
document.addEventListener('dragleave', function (event) {
    // Der ?. Operator wäre schicker aber der ACE Editor im Frickl macht das kaputt
    let dropziel = event.target.closest('.dropziel');
    let related = event.relatedTarget && event.relatedTarget.closest('.dropziel');
    if (dropziel != null && related == null) {
        anzeige4.innerHTML = 'dragleave: <br>"Sie gehen ja wieder weg!"';
        anzeige4.className = 'wiederweg';
        dropziel.classList.remove('erreicht');
    }
});

...
Sie können nur den unteren der beiden (Ab-)Sätze und das Bild ziehen, da beim oberen das Attribut draggable auf false gesetzt wurde.
Wichtig für die Funktion von Drag & Drop sind nur die beiden ersten Funktionen. Die übrigen dienen nur dazu, die Abläufe während der Operation zu kommentieren. Dazu dienen vier p-Elemente mit den IDs anzeige1 bis anzeige4, deren Elementobjekte für einfacheren Zugriff in Variablen gespeichert werden.
Bei dragenter und dragleave muss man beachten, dass das gezogene Element eventuell über Kindelemente des Dropbereichs gezogen wird. Wenn man in diesen Eventhandlern direkt auf event.target zugreift, würde man nicht auf den Dropbereich zugreifen, sondern auf eins seiner Kindelemente. Deswegen ist hier noch ein Aufruf von closest('.dropziel') nachgeschaltet, um die Elternkette soweit hochzugehen, bis das div für den Dropbereich gefunden wird (oder das Ende der Kette erreicht ist, wenn man außerhalb ist).
Bei dragleave kommt als weitere Besonderheit hinzu, dass dragleave auch dann feuert, wenn man in ein Kind-Element des Dropbereichs hineinzieht. Das ist nicht falsch, man verlässt den Dropbereich und betritt das Kindelement. Unser Beispiel erlaubt aber, auch auf Kindelemente des Dropbereichs zu droppen, deswegen wäre der Hinweis, dass man wieder weggeht, falsch, wenn man ein Kindelement betritt. Hier hilft die relatedTarget des Eventobjekts weiter. Bei einem dragleave Event findet sich hier das Element, in das hineingezogen wird. Die Abfrage lautet also: Verlässt man den Dropbereich (oder ein Kindelement davon) und zieht in etwas hinein, das nicht zum Dropbereich gehört? Nur dann hat man den Dropbereich wirklich verlassen.

DataTransfer Objekt

Der Vorgang des Ziehens wäre nicht möglich, ohne dass die Ereignisse in einem DataTransfer-Objekt gespeichert werden können. Deshalb haben alle drag-Events eine dataTransfer-Eigenschaft, die mit folgenden Methoden gesetzt und ausgelesen werden kann:

  • dataTransfer.effectAllowed=value: gibt den Typ der erlaubten Aktion zurück. Mögliche Werte sind:
    • none: Objekt darf nicht bewegt werden
    • copy: kopiert Objekt in die Dropzone
    • copyLink: copy oder link ist erlaubt
    • copyMove: copy oder move ist erlaubt
    • link: ein Link kann an der neuen Stelle eingerichtet werden
    • linkMove: link oder move ist erlaubt
    • move: darf bewegt werden
    • all: alle Funktionen sind erlaubt
    • uninitialized: alle Funktionen sind erlaubt
  • dataTransfer.setData(Format, Data): setzt Daten und ihr Format. Mögliche Werte wären:
    • text/plain
    • text/uri-list
    • image/jpeg
  • dataTransfer.getData(Format): gibt Datenwerte aus
  • dataTransfer.clearData(Format): entfernt gepeicherte Datenwerte
  • dataTransfer.setDragImage(Element, x, y): erzeugt ein ziehbares Bild, die x und y Koordinaten legen den Ort des Mauszeigers fest (0, 0 wäre die linke, obere Ecke).

Anwendungsbeispiel

Die oberen Beispiele sollten nur demonstrieren, wie wenig JavaScript-Code nötig ist. Im Folgenden sehen Sie nun, wie ein solches Beispiel logisch aufgebaut ist:

muss überarbeitet werden ansehen …
    <div id="div1" ondrop="ablegen(event)" ondragover="ablegenErlauben(event)">
      <p draggable="true" ondragstart="ziehen(event)" id="drag1"></p>
      <p draggable="true" ondragstart="ziehen(event)" id="drag2"></p>
      <p draggable="true" ondragstart="ziehen(event)" id="drag3"></p>
    </div>

    <div id="div2" ondrop="ablegen(event)" ondragover="ablegenErlauben(event)">
      <p draggable="true" ondragstart="ziehen(event)" id="drag4"></p>
      <p draggable="true" ondragstart="ziehen(event)" id="drag5"></p>
      <p draggable="true" ondragstart="ziehen(event)" id="drag6"></p>
    </div>
function ablegenErlauben(ev) {
    ev.preventDefault();
}

function ziehen(ev) {
    ev.dataTransfer.setData("text", ev.target.id);
}

function ablegen(ev) {
    ev.preventDefault();
    let data = ev.dataTransfer.getData("text");
    ev.target.appendChild(document.getElementById(data));
}
In diesem Beispiel können Sie die Kugeln (p-Elemente mit 50% border-radius) beliebig zwischen den weißen div-Feldern hin- und her schieben und ablegen.
Die Events sind mit Event-Attributen an die HTML-Elemente gehängt, im nächsten Beispiel zeigen wir Ihnen, wie es eleganter geht.

Grundschritte

Zuerst müssen Sie die Kugeln mittels draggable ziehbar machen.

Beispiel
<p draggable="true" id="kugel1"> </p>

Im oberen Beispiel wurden die Ereignisse durch Event-Attribute angehängt – hier wird das Ganze dynamisch erledigt:

Beispiel
window.addEventListener("load",function () {
  for (const draggable of document.querySelectorAll("[draggable=true]")) {
    draggable.addEventListener("dragstart",ziehen);
  };
});
Nach dem Laden des Dokuments werden mittels querySelectorAll alle Elemente mit dem Attribut draggable="true" ermittelt. Das Ergebnis ist eine NodeList, über die nun mit einer for...of Schleife itieriert wird. In der Schleife wird an jedes gefundene Element durch addEventListener der Event-Handler angebunden.


Beispiel
  function ziehen(ev) {
    ev.dataTransfer.setData('text', ev.target.id);
  }
Die Funktion ziehen() setzt die dataTransfer-Eigenschaft. Auch wenn in den Kugeln kein Text vorhanden ist, gelten sie doch als Text.

Ablegen

Sobald Sie sich mit dem gezogenen Objekt über der Zielzone befinden, wird das dragover-Event ausgelöst. Um das Ablegen zu ermöglichen, müssen Sie dieses mit preventDefault "abschalten". Erst dann darf abgelegt werden:

Beispiel ansehen …
function ablegenErlauben(ev) {
	ev.preventDefault();
}

function ablegen(event) {
    event.preventDefault();
    const kugelId = event.dataTransfer.getData('text');
    const target = event.currentTarget;
    target.appendChild(document.getElementById(kugelId));
}

document.addEventListener("DOMContentLoaded", function () {
    for (const zielzone of document.querySelectorAll(".zielzone")) {
        zielzone.addEventListener("drop", ablegen);
        zielzone.addEventListener("dragover", ablegenErlauben);
    };
    for (const draggable of document.querySelectorAll("[draggable=true]")) {
        draggable.addEventListener("dragstart", ziehen);
    };
});
In der Funktion ablegenErlauben() wird das die Browser-Reaktion auf das dragover-Event mit preventDefault abgeschaltet. Dann kann abgelegt werden. In der Funktion ablegen() wird der Wert der dataTransfer-Eigenschaft mit getData('text') ausgelesen. Beim Ziehen wird dort die ID der gezogenen Kugel hinterlegt, so dass sich die gezogene Kugel nun über ihre ID auffinden lässt. Zum Ablegen wird das DOM Element, das die Kugel abbildet, als neues Kind-Element des div eingefügt, in dem abgelegt wird.
Ist Ihnen aufgefallen, dass man im vorherigen Beispiel eine Kugel auch innerhalb einer anderen Kugel ablegen konnte?
Das lag daran, dass in der Funktion ablegen() das falsche Event-Target verwendet wurde. Die Eigenschaft target des Event-Objekts zeigt auf das Element, auf dem das Event ausgelöst wurde. Je nach Mausposition kann das das Ablage-div oder eine der anderen Kugeln sein. Es gibt aber auch die Eigenschaft currentTarget. Darin steht, auf welchem Element das Event behandelt wird (also wo der Eventhandler registriert ist). Nutzt man diese Eigenschaft, wird die Kugel immer in das div Element gesetzt.

Siehe auch

Weblinks

  1. W3C: Drag and Drop
  2. quirksmode: The HTML5 drag and drop disaster
  3. caniuse: Drag and Drop