JavaScript

D3.js für engagierte Angular-Entwickler, Teil 4

Drag and Drop mit Angular realisieren: Tipps und Tricks
Keine Kommentare

Drag and Drop im Browser: Früher kaum umsetzbar, heute nicht mehr aus dem Web wegzudenken. Vor allem bei Devices mit Touch-Display kommt man nicht drum herum. Die Umsetzung kann allerdings kniffelig werden. Wie implementiert man eine Drag-and-Drop-Funktion mit Angular und D3.js? Heute betrachten wir erst mal die Angular-Seite der Angelegenheit.

Wenn ich mich an früher zurückerinnere, an die Zeiten als ich das erste Mal mit der Anforderung von Drag and Drop konfrontiert wurde… Damals waren noch der Internet Explorer 6 und andere ältere Browser bei unseren Kunden im Einsatz. Zu dieser Zeit war eine Umsetzung fast unmöglich. Entweder hat es nicht richtig funktioniert oder die Rechenlast war enorm – von Drag and Drop zwischen dem Browser und anderen Anwendungen mal ganz abzusehen.

Teil 1: Drag and Drop mit Angular realisieren

Glücklicherweise ändern sich die Zeiten. Mit den HTML5 Erweiterungen zum Thema Drag and Drop, den D3.js-Routinen für Animationen und natürlich auch moderneren Browsern, können wir solche Anforderungen mit überschaubarem Aufwand angehen. Gerade in Hinblick auf die Usability und mobile bzw. touch-fähige Geräte wird Drag and Drop immer wichtiger.

Um es gleich zu sagen: Die Implementierung von Drag and Drop, insbesondere mit eigenen Animationen, ist nach wie vor aufwändig und nicht mit ein paar Zeilen Quellcode erledigt. Aber es ist machbar. Insbesondere wenn man per Drag and Drop über den Browser hinweg mit anderen Anwendungen kommunizieren will, wird man häufig um eine eigene Implementierung nicht herumkommen. Und wie das funktionieren kann, werden wir nachfolgend gemeinsam erschließen.

D3.js für engagierte Angular-Entwickler – die Artikelserie

Teil 1: Bubble-Charts
Teil 2: Pie-Charts
Teil 3: Animierte Diagramme
Teil 4: Drag & Drop mit Angular
Teil 5: Drag & Drop mit eigenem Ghost
Teil 6: Teil 6: Drag & Drop mit Animation in D3.js

Der Quellcode des hier beschriebenen Beispiels steht auf Github mit zusätzlichen Kommentaren zum Nachlesen zur Verfügung.

Die Grundstruktur erzeugen

Wir erzeugen zunächst ein TypeScript-Interface, das unsere verschiebbaren Elemente beschreibt. Wir wollen, dass die Elemente einen Text beinhalten, eine individuelle Hintergrundfarbe besitzen und dass diese absolut positioniert werden können.

 
interface Item {
  text: string;
  color: string;
  left: number;
  top: number;
}

Weiterhin kann unser Element entweder noch verschiebbar oder bereits verschoben worden sein. Für diese beiden Zustände erzeugen wir in der Angular-Komponente jeweils ein Array.

 
public draggable: Item[] = [];
public dropped: Item[] = [];

Innerhalb der Komponenten-Methode ngOnInit initialisieren wir unsere Elemente. Da die Elemente noch nicht verschoben wurden, legen wir sie innerhalb unseres ersten Arrays draggable an.

 
ngOnInit() {
  this.draggable = [
    { text: 'Item 1', color: 'red', left: 100, top: 100 },
    { text: 'Item 2', color: 'green', left: 200, top: 100 },
    { text: 'Item 3', color: 'yellow', left: 300, top: 100 }
  ]
}

In unserem HTML-Template erzeugen wir noch das Grundgerüst.

 
<div class="drag-container">
  <div class="drop-zone">
    <div *ngFor="let item of dropped"
      class="dropped-item"
      [style.background-color]="item.color"
    >
      {{ item.text }}
    </div>
  </div>
  <div *ngFor="let item of draggable" 
    class="draggable-item"
    [style.left]="item.left+'px'"
    [style.top]="item.top+'px'"
    [style.background-color]="item.color"
  >
    {{ item.text }}
  </div>
</div>

Anschließend noch einige einfache CSS-Styles und wir können unsere Implementierungen erstmals betrachten.

 
/* styles.css */
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
}
 
/* *.component.css */
.drag-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.drop-zone {
  height: 60px;
  background-color: #dadada;
}

.draggable-item {
  position: absolute;
  width: 80px;
  height: 80px;
  border-radius: 50%;
  text-align: center;
  line-height: 80px;
}

.dropped-item {
  text-align: center;
  line-height: 20px;
}

Unsere Elemente verschiebbar machen

Dank der HTML5-Erweiterung zu dem Thema Drag and Drop lassen sich Elemente relativ einfach verschiebbar machen.

Im ersten Schritt müssen die Elemente, die verschiebbar sein sollen, entsprechend gekennzeichnet werden. Dies erfolgt durch das HTML-Attribut draggable=“true“. Dadurch werden auf den entsprechenden Elementen die Ereignisse dragstart und dragend verfügbar.

 
<div *ngFor="let item of draggable" 
  class="draggable-item"
  [style.left]="item.left+'px'"
  [style.top]="item.top+'px'"
  [style.background-color]="item.color"
  draggable="true"
  (dragstart)="dragStart($event, item)"
  (dragend)="dragEnd($event, item)"
>
  {{ item.text }}
</div>

Das dragstart-Ereignis wird ausgelöst, sobald ein Drag-Vorgang eingeleitet wird. Das dragend-Ereignis hingegen, wenn der Drag-Vorgang abgebrochen wurde. Während des Drag-Vorgangs wollen wir die Referenz auf das entsprechende Element in dragging sichern, um später darauf zugreifen zu können. Dazu initialisieren wir diese Referenz im dragstart-Ereignis und entfernen sie wieder im dragend-Ereignis.

 
public dragging: Item;

public dragStart(event: DragEvent, item: Item){
  this.dragging = item;
};

public dragEnd(event: DragEvent, item: Item){
  this.dragging = undefined;
};

Aber wohin mit den Elementen?

Die Elemente lassen sich nun bereits anfassen und verschieben, aber wir können sie nirgendwo fallen lassen. Dafür müssen wir zwei weitere Ereignisse definieren, diesmal auf unserer Landezone – der drop-zone.

 
<div class="drop-zone"
    (dragover)="dragOver($event)"
    (drop)="drop($event)"
  >

Das Ereignis dragover wird fortlaufend ausgelöst, während wir ein Element über unserer Landezone verschieben. Standardmäßig wird kein HTML-Element auf Drag-Vorgänge reagieren. Dies lässt sich jedoch durch den Aufruf der Methode preventDefault des Ereignis-Objektes ändern. Wir müssen allerdings beachten, dass nicht nur unsere eigenen Elemente auf die Landezone verschoben werden können. Der Anwender kann auch Grafiken oder Dateien von seinem Rechner nehmen und diese dort fallen lassen. Zur Absicherung prüfen wir also anhand dragging ab, ob der Drag-Vorgang durch eines unserer Elemente ausgelöst wurde.

 
public dragOver(event: DragEvent){
  if(this.dragging){
    event.preventDefault();
  }
};

Des Weiteren müssen wir noch darauf reagieren, wenn das Element tatsächlich über unserer Landezone fallen gelassen wird. Das implementieren wir im Ereignis drop. Wenn wir das Element fallen lassen, wollen wir es zunächst aus dem Array draggable in das Array dropped verschieben. Dafür greifen wir auf unsere Referenz in dragging zurück. Wir dürfen natürlich nicht vergessen, diese Referenz am Ende auch wieder zurück zu setzen.

 
public drop(event: DragEvent){
  const index = this.draggable.indexOf(this.dragging);
  this.draggable.splice(index, 1);
  this.dropped.push(this.dragging);
  this.dragging = undefined;
};

Funktioniert das Browser-übergreifend?

Nun testen wir unsere bisherige Implementierung. Nanu, im Firefox passiert nichts und im Internet Explorer wird ein Kopieren-Symbol angezeigt? Nur der Chrome verhält sich so wie wir das erwartet haben…

Glücklicherweise können wir das Problem schnell beheben. Da die Drag-and-Drop-Funktionalität nicht nur auf den Browser beschränkt ist, sondern wir Inhalte per Drag and Drop zwischen Browser und anderen Anwendungen austauschen können, müssen wir noch den zu kommunizierenden Inhalt definieren. Dies erfolgt einfach über die setData-Methode innerhalb des Ereignisses dragstart. Hier übergeben wir einfachheitshalber die jeweilige Betextung unseres Elements. Die Art des Drag-Vorgangs können wir über die Eigenschaft effectAllowed definieren. Hier können unter anderem die Werte copy für Kopieren oder move für Verschieben angegeben werden – in unserem Fall entscheiden wir uns für move.

 
public dragStart(event: DragEvent, item: Item){
  event.dataTransfer.setData('text', item.text);
  event.dataTransfer.effectAllowed = 'move';
  this.dragging = item;
};

Testen wir nun wieder unsere Änderung. Im Internet Explorer funktioniert nun alles einwandfrei aber im Firefox bekommen wir nach dem Drag-Vorgang die Startseite angezeigt? Im Prinzip ist die Reaktion des Firefox logisch, wenn wir eine URL – die eigentlich ja auch nur ein Text ist – per Drag and Drop in den Browser ziehen, wird diese aufgerufen. Der Firefox unterscheidet hier nicht zwischen einem einfachen Text und einer URL. Das Verhalten des Firefox können wir aber durch ergänzen der Methode preventDefault in unserem Ereignis drop umgehen.

 
public drop(event: DragEvent){
    event.preventDefault();
    const index = this.draggable.indexOf(this.dragging);
    this.draggable.splice(index, 1);
    this.dropped.push(this.dragging);
    this.dragging = undefined;
  };

Fazit und Ausblick

Geschafft, wir haben unsere Drag-and-Drop-Funktionalität implementiert – war eigentlich doch gar nicht so schwer, oder? Im nächsten Teil dieser Serie werden wir versuchen, das Standard-Browser-Ghost-Element durch ein individuelles Ghost-Element zu ersetzen, als Vorbereitung für unsere Animationen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -