Freaky Friday: Native Drag ’n Drop

Es ist schon wieder Freitag und diese Woche habe ich ein kleines Highlight für Euch. Einige von Euch haben sich sicherlich schon immer gefragt, wie das mit diesem Drag ’n Drop eigentlich funktioniert. Wie schaffen es Webseiten, wie zum Beispiel Facebook beim Foto Upload, dass man Dateien einfach vom Desktop in den Browser ziehen und ablegen kann? Die Lösung ist verdammt einfach. Jeder aktuelle, moderne Browser unterstützt die HTML Drag and Drop API. Ich zeige Euch heute, wie das funktioniert.

Drag and Drop

Dann schreiben wir uns mal unser eigenes Upload Tool. Geplant ist ein einfacher Bildupload mit Fortschrittsanzeige und Darstellung der hochgeladenen Bilder. So könnte unser HTML aussehen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Dropzone -->
<div class="mmn-dropzone">
    Hier die Dateien per Drag and Drop ablegen
</div>
 
<!-- Progress Bar -->
<div class="mmn-progress">
    <div class="mmn-progress-bar" role="progressbar" style="width:0%">&nbsp;</div>
    <span>0%</span>
</div>
 
<!-- Bild Container -->
<div class="mmn-uploaded-images">
 
</div>

Was haben wir hier also. Im Grunde genommen kommen wir mit drei einfachen DIV Containern aus. Die Dropzone ist der Ablagebereich, in dem wir unsere Dateien mittels Drag and Drop ablegen. Die Progress Bar ist einfach die Fortschrittsanzeige, die uns den Status des Bilduploads anzeigt. Wegen der mangelhaften Browserunterstützung des HTML5 progress-Elements habe ich hier einfach auf einen einfachen DIV-Container zurückgegriffen. Somit können wir die Progress Bar auch vernünftig stylen. Der dritte Container dient zur Darstellung der Thumbnails der hochgeladenen Bilder.

Stylen wir das ganze ein wenig mit CSS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.mmn-dropzone { 
    color: #ccc; 
    border-radius: 10px; 
    border: 2px dashed #ccc; 
    height: 200px; 
    line-height: 200px; 
    margin-bottom: 20px; 
    text-align: center; 
}
.mmn-dropzone.mmn-drop { 
    border-color: #000; 
    color: #000; 
}
.mmn-progress { 
    border: 1px solid #000; 
    height: 2em; 
    text-align: center; 
}
.mmn-progress span { 
    position: relative; 
    top: -2em; 
}
.mmn-progress-bar { 
    background-color: #ccc; 
}
.mmn-thumbnail { 
    border: 1px solid #000; 
    overflow: hidden; 
    padding: 10px; 
    margin-bottom: 10px; 
}
.mmn-thumbnail img { 
    display: inline-block; 
    float: left; 
    margin-right: 10px; 
    max-height: 150px; 
    max-width: 200px; 
    filter: grayscale(1); 
    -webkit-filter: grayscale(1); 
}
.mmn-thumbnail img:hover { 
    filter: grayscale(0); 
    -webkit-filter: grayscale(0); 
}

Euch ist vielleicht aufgefallen, dass bei den CSS Klassen bereits eine Klasse .mmn-thumbnail mit aufgeführt wurde. Diese Klasse stylt einfach unsere Thumbnails Dazu wird es später mehr geben. Konzentrieren wir uns erstmal auf die Dropzone. Wie sage ich einem Browser, was er machen soll, wenn ich eine Datei in der Dropzone ablege. Hierzu stellt die Drag and Drop API die entsprechenden Events zur Verfügung. Für unsere Zwecke werfen wir einfach mal einen Blick auf die Events drop, dragover und dragleave. Das Event dragover beschreibt den Zustand des in die Dropzone Hineinziehens. Solang ich mich mit der Mouse in der Dropzone befinde, wird das dragover Event abgefeuert. Das dragleave Event ist das entsprechende Pendant zu dragover. Es wird abgefeuert, wenn ich die Dropfzone verlasse. Ich benutze diese beiden Events, um einen Hover Effekt darzustellen. Das drop Event ist das Event, welches das Ablegen einer Datei in der Dropzone beschreibt. Dieses Event löst unseren Upload Prozess aus.

Events, Events, Events …

Wie sieht unser Javascript aus, wenn wir die gerade beschriebenen Events beachten wollen? Es könnte zum Beispiel so aussehen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var dropZone = document.querySelector('.mmn-dropzone');
 
dropZone.addEventListener('drop', function(event) {
    event.preventDefault();
    this.classList.remove('mmn-drop');
    startUpload(event.dataTransfer.files);
}, false);
 
dropZone.addEventListener('dragover', function(event) {
    event.preventDefault();
    this.classList.add('mmn-drop');
}, false);
 
dropZone.addEventListener('dragleave', function(event) {
    event.preventDefault();
    this.classList.remove('mmn-drop');
}, false);

Unsere Dropzone bekommt hier die Event Listener für die oben genannten Events. Für die Events dragover und dragleave wird lediglich eine CSS Klasse zur Dropzone hinzugefügt oder wieder entfernt. Theoretisch könnten wir diese Events auch außer Acht lassen und diesen optischen Effekt mit der CSS hover Pseudoklasse erzeugen. Zur Veranschaulichung der verschiedenen Events soll das heute aber einfach mal reichen.

Das drop Event löst eine weitere Javascript Funktion aus, die den Upload der in der Dropzone abgelegten Dateien startet. Dieses Event wird immer abgefeuert, sobald der User die Mouse loslässt. Sobald der User die Mouse loslässt, sind alle Drop Events beendet. Das drop Event besitzt nach dem Loslassen der Mouse eine dataTransfer Eigenschaft, die die in der Dropzone abgelegten Daten enthält. In diesem Fall also unsere Dateien, die wir an unsere Upload Funktion übergeben.

Loading …

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var formdata = new FormData(),
    mimeTypes = [ 'image/jpeg', 'image/png', ],
    progressBar = document.querySelector('.mmn-progress-bar'),
    validFiles = [],
    xhr = new XMLHttpRequest();
 
var startUpload = function(files) {
    if (files.length > 20) {
        setMessage('Oh oh!', 'Es dürfen maximal 20 Dateien abgelegt werden. Du versuchst gerade ' + files.length + ' Bilder hochzuladen.', 'mmn-error');
    }
 
    for (var i = 0; i < files.length; i++) {
        if (mimeTypes.indexOf(files.item(i).type) == -1) {
            setMessage('Doh!', 'Die Datei ' + files.item(i).name + ' ist kein Bild. Du darfst hier nur Bilder (jpg und png) hochladen.', 'mmn-error');
	} else if (files.item(i).size > 1000000) {
	    setMessage('Verdammt!', 'Die Datei ' + files.item(i).name + ' ist zu groß. Ein Bild darf maximal 1MB groß sein.', 'mmn-error');
        } else { 
            validFiles.push(files.item(i));
            formdata.append('mmnfiles[]', files.item(i), files.item(i).name);
        }
    }
 
    xhr.upload.addEventListener('progress', function(event) {
        var percentComplete = Math.ceil(event.loaded / event.total * 100);
        progressBar.style.width = percentComplete + '%';
        progressBar.nextElementSibling.textContent = percentComplete + '%';
    });
 
    xhr.onload = function() {
        if (xhr.status === 200) {
            data = JSON.parse(xhr.responseText);
            if (data.type == 'success') {
                setMessage('Yeah!', data.message, 'mmn-success');
            }
 
            if (data.type == 'error') {
                setMessage('Oh oh!', data.message, 'mmn-error');
            }
 
            for (var i = 0; i < validFiles.length; i++) {
                setImage(validFiles[i]);
            }
        }
    }
 
    xhr.open('POST', 'upload.php');
    xhr.send(formdata);
}

Die startUpload Javascript Funktion mag auf den ersten Blick ein wenig komplex aussehen. Allerdings erledigt sie auch den eigentlichen Job unseres kleinen Projektes. Der übergebene files Parameter ist ein FileList Objekt, welches die einzelnen Dateien als Javascript File Objekt unserer Drag and Drop Aktion enthält. Wir durchlaufen das FileList Objekt einfach und führen für jede einzelne Datei erstmal ein paar sicherheitsrelevante Checks durch.

Zunächst prüfen wir erstmal, wieviele Dateien auf den Server geladen werden sollen. Da wir die Dateien mittels eines PHP Scripts auf dem server ablegen möchten, müssen wir hier einiges beachten. Seit PHP 5.2.12 existiert die max_file_uploads Einstellung. Diese ist meist auf 20 Dateien eingestellt. Sollte die Anzahl der gleichzeitig hochzuladenden Dateien überschritten werden, resultiert dies in einem PHP Fehler und der Upload wird nicht gestartet. Aus diesem Grund fange ich diese Fehlerquelle direkt im Browser ab und setze eine entsprechende Fehlermeldung.

Mit dem JavaScript File Objekt können wir verschiedene Eigenschaften einer Datei schon vor dem eigentlichen Upload berücksichtigen. Ich überprüfe hier einmal den Mime Type einer Datei. Entspricht dieser nicht einem JPG oder PNG Bildformat, wird für die entsprechende Datei ein Fehler gesetzt. Diese Datei wird nicht auf den Server geladen. Darüber hinaus überprüfe ich die Dateigröße. In diesem Beispiel habe ich die maximal zugelassene Dateigröße auf 1MB beschränkt. Letztendlich könnte man hier auch auf die PHP Einstellung upload_max_filesize zurückgreifen. Ist die Datei größer als 1MB wird sie ebenfalls nicht auf den Server geladen.

Die validen Dateien speichere ich im Javascript FormData Objekt, welches ich dann später mittels einem AJAX Request an den Server sende.

Fortschritt!

In Zeile 23 der oben gezeigten startUpload Funktion gehen wir unsere Progress Bar an. Diese soll schließlich den Fortschritt des Uploads anzeigen. Dafür hat das XMLHttpRequest Objekt ein progress-Event, welches genau für unsere Zwecke gemacht ist. Wir bekommen darüber die Anzahl der gesamten Menge, die auf den Server zu laden ist und die Anzahl der Menge, die bereits auf den Server geladen ist. Daraus lässt sich relativ einfach errechnen, wieviel Prozent des Uploads bereits erledigt sind.

Ab Zeile 29 werten wir die Antwort des Servers aus. Das von mir eingesetzte PHP Script führt eigentlich die gleichen Prüfungen, die wir hier auf Client-Seite schon durchgeführt haben, nochmals auf der Seite des Servers aus. Es wird die Dateigröße geprüft. Weiterhin wir der Mime Type nochmals überprüft und es wird auf eventuelle Fehler beim Upload geprüft. Also alles das, was man so oder so bei einem Bildupload mit PHP prüfen sollte. Sobald die Datei auch serverseitig von PHP als valide erachtet wird, wird sie auf dem Server gespeichert. Das PHP Script liefert eine Response im JSON Format. Diese werten wir aus. Ist ein Fehler gemeldet worden, wird ein Fehler dargestellt. Wird ein Erfolg gemeldet, wird auch dies dargestellt und wir stellen nun die Thumbnails dar.

Zuguterletzt senden wir die Daten im FormData Objekt mittels AJAX an die PHP Datei auf dem Server, welche die Daten verarbeitet und auf dem Server ablegt.

HTML5 Templates

Sowohl für die Fehler- und Erfolgsmeldungen, als auch für die Thumbnails der hochgeladenen Bilder habe ich das HTML5 template-Element verwendet. Eine simple Möglichkeit wiederkehrende HTML Elemente wiederzuverwenden. Ich werde das jetzt hier einmal an dem Thumbnail Template erklären, da hier auch eine sehr wichtige Javascript Funktion zur Darstellung von Dateiinhalten eine wichtige Rolle spielt.

1
2
3
4
5
6
<template id="mmn-image-template">
    <div class="mmn-thumbnail">
        <img src="" alt=""/>
        <span></span>
    </div>
</template>

Das ist unser Thumbnail Template. Ganz bewusst sind hier noch keine Daten enthalten, da diese erst in das Template eingesetzt werden, sobald bekannt ist, welche Dateien auf den Server geladen wurden. Dargestellt werden soll das Bild und der Dateiname des Bildes. Die Javascript Funktion, welche die Daten in das Template einsetzt und es letztendlich in das vorhandene DOM einbindet, sieht folgendermaßen aus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var setImage = function(file) {
    var imageTemplate = document.getElementById('mmn-image-template')
    clone = document.importNode(imageTemplate.content, true);
 
    var image = clone.querySelector('img');
 
    var reader = new FileReader();
    reader.onload = (function(element) { 
        return function(event) { 
            element.src = event.target.result; 
        }; 
    })(image);
    reader.readAsDataURL(file);
 
    image.nextElementSibling.textContent = file.name;
    thumbnailContainer.appendChild(clone);
}

Wir lesen das Template ein und erzeugen vom Template einen Clone, um diesen mit Daten bestücken zu können. Würde wir das Template direkt mit Daten bestücken, addieren sich die Daten und beim letzten Bild hätten wir dann alle Dateinamen in einem langen String. Das wollen wir so nicht und deswegen gibt es hier direkt den Clone. Der Clou der ganzen Sache ist, dass wir die Dateiinhalte schon im Browser ermitteln und nicht auf die Response des Servers zurückgreifen. Mit dem FileReader Objekt können wir schon während der Laufzeit des Scripts asynchron auf die Bildinhalte zugreifen und diese dann im src-Attribute des img-Elements unseres Templates hinterlegen. Dies geht sehr viel schneller, als die Bildinhalte mittels PHP in der Response zurück zu liefern. Somit bleibt die Response des Servers so schmal wie möglich. Im Grunde genommen könnten wir die validen Thumbnails auch schon darstellen, ohne die Response des Servers abwarten zu müssen, da diese bereits direkt nach dem drop Event mit dem FileList Objekt zur Verfügung stehen.

Praktisches Beispiel

Natürlich gibt es auch wieder ein praktisches Beispiel. Dieses Beispiel macht genau das, was in diesem Artikel beschrieben wurde.

Fazit

Javascript hat sich über die Jahre ganz schön gemacht. Zudem haben sich die Browser auch enorm weiter entwickelt. Das angegebene Beispiel habe ich jetzt lediglich im aktuellen Edge Browser von Microsoft, im aktuellen Chrome Browser von Google und im Mozilla Firefox 50 getestet. Ihr solltet dieses Beispiel also nicht direkt produktiv nutzen, wenn ihr auch für den Internet Explorer entwickeln müsst. Wenn jemand Lust hat veraltete Browser zu testen, kann er dazu gern ein kleines Feedback in den Kommentaren hinterlassen. Es fehlt definitiv ein Fallback für ältere Browser im Beispiel. Wunderbar finde ich persönlich, dass ich kein großes Javascript Framework mit mir rumschleppen muss. Die beispielhafte HTML Datei ist gerade mal 8KB groß und funktioniert entsprechend schnell. Ich bin begeistert!

Haben wir wieder was gelernt. 😉

About Author: Marcel
Ich bin Senior PHP Developer bei MM Newmedia. Seit 2005 bin ich begeisterter Webentwickler und arbeite als Freelancer für namenhafte Firmen und entwickle jede Menge abgefahrenes Zeug und berichte darüber in meinem Blog.

Kommentar verfassen